1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-20 20:10:15 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java

888 lines
37 KiB
Java
Raw Normal View History

/* Copyright (C) 2023-2024 Andreas Böhler, Daniel Dakhno, José Rebelo
2023-08-31 23:23:10 +02:00
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
2023-08-31 23:23:10 +02:00
package nodomain.freeyourgadget.gadgetbridge.activities.discovery;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast;
import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Pair;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.core.app.ActivityCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.DebugActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
2023-08-31 23:23:10 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener,
BondingInterface,
GBScanEventProcessor.Callback {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivityV2.class);
private final Handler handler = new Handler();
private static final long SCAN_DURATION = 30000; // 30s
private static final long LIST_REFRESH_THRESHOLD_MS = 1000L;
2023-08-31 23:23:10 +02:00
private long lastListRefresh = System.currentTimeMillis();
private final ScanCallback bleScanCallback = new BleScanCallback();
private ProgressBar bluetoothProgress;
private ProgressBar bluetoothLEProgress;
private DeviceCandidateAdapter deviceCandidateAdapter;
private GBDeviceCandidate deviceTarget;
private BluetoothAdapter adapter;
private Button startButton;
private boolean scanning;
private long selectedUnsupportedDeviceKey = DebugActivity.SELECT_DEVICE;
private final Runnable stopRunnable = () -> {
stopDiscovery();
LOG.info("Discovery stopped by thread timeout.");
};
private final BroadcastReceiver bluetoothReceiver = new BluetoothReceiver();
private final GBScanEventProcessor deviceFoundProcessor = new GBScanEventProcessor(this);
// Array to back the adapter for the UI
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
@RequiresApi(Build.VERSION_CODES.O)
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
}
@Nullable
private GBDeviceCandidate getCandidateFromMAC(final BluetoothDevice device) {
for (final GBDeviceCandidate candidate : deviceCandidates) {
if (candidate.getMacAddress().equals(device.getAddress())) {
return candidate;
}
}
LOG.warn("This shouldn't happen unless the list somehow emptied itself, device MAC: {}", device.getAddress());
return null;
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadSettings();
setContentView(R.layout.activity_discovery);
startButton = findViewById(R.id.discovery_start);
startButton.setOnClickListener(v -> toggleDiscovery());
final Button settingsButton = findViewById(R.id.discovery_preferences);
settingsButton.setOnClickListener(v -> {
final Intent enableIntent = new Intent(DiscoveryActivityV2.this, DiscoveryPairingPreferenceActivity.class);
startActivity(enableIntent);
});
bluetoothProgress = findViewById(R.id.discovery_progressbar);
bluetoothProgress.setProgress(0);
bluetoothProgress.setIndeterminate(true);
bluetoothProgress.setVisibility(View.GONE);
bluetoothLEProgress = findViewById(R.id.discovery_ble_progressbar);
bluetoothLEProgress.setProgress(0);
bluetoothLEProgress.setIndeterminate(true);
bluetoothLEProgress.setVisibility(View.GONE);
deviceCandidateAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
final ListView deviceCandidatesView = findViewById(R.id.discovery_device_candidates_list);
deviceCandidatesView.setAdapter(deviceCandidateAdapter);
deviceCandidatesView.setOnItemClickListener(this);
deviceCandidatesView.setOnItemLongClickListener(this);
registerBroadcastReceivers();
checkAndRequestLocationPermission();
if (!startDiscovery()) {
/* if we couldn't start scanning, go back to the main page.
A toast will have been shown explaining what's wrong */
finish();
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
final List<Parcelable> restoredCandidates = savedInstanceState.getParcelableArrayList("deviceCandidates");
if (restoredCandidates != null) {
deviceCandidates.clear();
for (final Parcelable p : restoredCandidates) {
final GBDeviceCandidate candidate = (GBDeviceCandidate) p;
deviceCandidates.add(candidate);
}
}
}
@Override
protected void onDestroy() {
unregisterBroadcastReceivers();
stopDiscovery();
super.onDestroy();
}
@Override
protected void onStop() {
unregisterBroadcastReceivers();
stopDiscovery();
super.onStop();
}
@Override
protected void onPause() {
unregisterBroadcastReceivers();
stopDiscovery();
super.onPause();
}
@Override
protected void onResume() {
loadSettings();
registerBroadcastReceivers();
super.onResume();
}
private void refreshDeviceList(final boolean throttle) {
handler.post(() -> {
if (throttle && System.currentTimeMillis() - lastListRefresh < LIST_REFRESH_THRESHOLD_MS) {
return;
}
LOG.debug("Refreshing device list");
// Clear and re-populate the list. deviceFoundProcessor keeps insertion order, so newer devices
// will still be at the end
deviceCandidates.clear();
deviceCandidates.addAll(deviceFoundProcessor.getDevices());
deviceCandidateAdapter.notifyDataSetChanged();
lastListRefresh = System.currentTimeMillis();
});
}
private void toggleDiscovery() {
if (scanning) {
stopDiscovery();
} else {
startDiscovery();
}
}
private boolean startDiscovery() {
if (scanning) {
LOG.warn("Not starting discovery, because already scanning.");
return false;
}
LOG.info("Starting discovery");
startButton.setText(getString(R.string.discovery_stop_scanning));
deviceFoundProcessor.clear();
deviceFoundProcessor.start();
refreshDeviceList(false);
try {
if (!ensureBluetoothReady()) {
toast(DiscoveryActivityV2.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
return false;
}
if (GB.supportsBluetoothLE()) {
startBTLEDiscovery();
}
startBTDiscovery();
} catch (final SecurityException e) {
LOG.error("SecurityException on startDiscovery");
deviceFoundProcessor.stop();
return false;
}
setScanning(true);
return true;
}
private void stopDiscovery() {
LOG.info("Stopping discovery");
try {
stopBTDiscovery();
stopBLEDiscovery();
} catch (final SecurityException e) {
LOG.error("SecurityException on stopDiscovery");
}
setScanning(false);
deviceFoundProcessor.stop();
handler.removeMessages(0, stopRunnable);
// Refresh the device list one last time when finishing
refreshDeviceList(false);
}
public void setScanning(final boolean scanning) {
this.scanning = scanning;
if (scanning) {
startButton.setText(getString(R.string.discovery_stop_scanning));
} else {
startButton.setText(getString(R.string.discovery_start_scanning));
bluetoothProgress.setVisibility(View.GONE);
bluetoothLEProgress.setVisibility(View.GONE);
}
}
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
private void startBTLEDiscovery() {
LOG.info("Starting BLE discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
// Filters being non-null would be a very good idea with background scan, but in this case,
// not really required.
// TODO getScanFilters maybe
adapter.getBluetoothLeScanner().startScan(null, getScanSettings(), bleScanCallback);
LOG.debug("Bluetooth LE discovery started successfully");
bluetoothLEProgress.setVisibility(View.VISIBLE);
}
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
private void stopBLEDiscovery() {
if (adapter == null) {
return;
}
final BluetoothLeScanner bluetoothLeScanner = adapter.getBluetoothLeScanner();
if (bluetoothLeScanner == null) {
LOG.warn("Could not get BluetoothLeScanner()!");
return;
}
if (bleScanCallback == null) {
LOG.warn("newLeScanCallback == null!");
return;
}
try {
bluetoothLeScanner.stopScan(bleScanCallback);
} catch (final NullPointerException e) {
LOG.warn("Internal NullPointerException when stopping the scan!");
return;
}
LOG.debug("Stopped BLE discovery");
}
/**
* Starts a regular Bluetooth scan
*/
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
private void startBTDiscovery() {
LOG.info("Starting BT discovery");
try {
// LineageOS quirk, can't stop scan properly,
// if scan has been started by something else
stopBTDiscovery();
} catch (final Exception ignored) {
}
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
if (adapter.startDiscovery()) {
LOG.debug("Discovery started successfully");
bluetoothProgress.setVisibility(View.VISIBLE);
} else {
LOG.error("Discovery starting failed");
}
}
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
private void stopBTDiscovery() {
if (adapter == null) return;
adapter.cancelDiscovery();
LOG.info("Stopped BT discovery");
}
private void bluetoothStateChanged(final int newState) {
if (newState == BluetoothAdapter.STATE_ON) {
this.adapter = BluetoothAdapter.getDefaultAdapter();
startButton.setEnabled(true);
} else {
this.adapter = null;
startButton.setEnabled(false);
bluetoothProgress.setVisibility(View.GONE);
bluetoothLEProgress.setVisibility(View.GONE);
}
}
private boolean checkBluetoothAvailable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
LOG.warn("No BLUETOOTH_SCAN permission");
this.adapter = null;
return false;
}
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
LOG.warn("No BLUETOOTH_CONNECT permission");
this.adapter = null;
return false;
}
}
final BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
if (bluetoothService == null) {
LOG.warn("No bluetooth service available");
this.adapter = null;
return false;
}
final BluetoothAdapter adapter = bluetoothService.getAdapter();
if (adapter == null) {
LOG.warn("No bluetooth adapter available");
this.adapter = null;
return false;
}
if (!adapter.isEnabled()) {
LOG.warn("Bluetooth not enabled");
final Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableBtIntent);
this.adapter = null;
return false;
}
this.adapter = adapter;
return true;
}
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
private boolean ensureBluetoothReady() {
final boolean available = checkBluetoothAvailable();
startButton.setEnabled(available);
if (available) {
adapter.cancelDiscovery();
// must not return the result of cancelDiscovery()
// appears to return false when currently not scanning
return true;
}
return false;
}
private static ScanSettings getScanSettings() {
final ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
builder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
2023-08-31 23:23:10 +02:00
builder.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED);
}
return builder.build();
}
private List<ScanFilter> getScanFilters() {
final List<ScanFilter> allFilters = new ArrayList<>();
for (DeviceType deviceType : DeviceType.values()) {
allFilters.addAll(deviceType.getDeviceCoordinator().createBLEScanFilters());
2023-08-31 23:23:10 +02:00
}
return allFilters;
}
private Message getPostMessage(final Runnable runnable) {
final Message message = Message.obtain(handler, runnable);
message.obj = runnable;
return message;
}
private void checkAndRequestLocationPermission() {
/* This is more or less a copy of what's in ControlCenterv2, but
we do this in case the permissions weren't requested since there
is no way we can scan without this stuff */
List<String> wantedPermissions = new ArrayList<>();
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access coarse location!");
wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
}
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access fine location!");
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
// if we need location permissions, request both together to avoid a bunch of dialogs
if (wantedPermissions.size() > 0) {
toast(DiscoveryActivityV2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
wantedPermissions.clear();
}
// Now we have to request background location separately!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access background location!");
toast(DiscoveryActivityV2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
}
}
// Now, we can request Bluetooth permissions....
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access Bluetooth scanning!");
toast(DiscoveryActivityV2.this, getString(R.string.error_no_bluetooth_scan), Toast.LENGTH_SHORT, GB.ERROR);
wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access Bluetooth connection!");
toast(DiscoveryActivityV2.this, getString(R.string.error_no_bluetooth_connect), Toast.LENGTH_SHORT, GB.ERROR);
wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}
}
if (wantedPermissions.size() > 0) {
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
} else {
ActivityResultLauncher<String[]> requestMultiplePermissionsLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {
if (!isGranted.containsValue(false)) {
// Permission is granted. Continue the action or workflow in your app.
// should we do startDiscovery here??
} else {
// Explain to the user that the feature is unavailable because the feature requires a permission that the user has denied.
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
}
});
requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0]));
}
}
LocationManager locationManager = (LocationManager) DiscoveryActivityV2.this.getSystemService(Context.LOCATION_SERVICE);
try {
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
// Do nothing
LOG.debug("Some location provider is enabled, assuming location is enabled");
} else {
toast(DiscoveryActivityV2.this, getString(R.string.require_location_provider), Toast.LENGTH_LONG, GB.ERROR);
DiscoveryActivityV2.this.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
// We can't be sure location was enabled, cancel scan start and wait for new user action
toast(DiscoveryActivityV2.this, getString(R.string.error_location_enabled_mandatory), Toast.LENGTH_SHORT, GB.ERROR);
return;
}
} catch (final Exception ex) {
LOG.error("Exception when checking location status", ex);
}
LOG.info("Permissions seems to be fine for scanning");
}
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) {
final GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
LOG.error("Device candidate clicked, but item not found");
return;
}
DeviceType deviceType = DeviceHelper.getInstance().resolveDeviceType(deviceCandidate);
if (!deviceType.isSupported()) {
2023-08-31 23:23:10 +02:00
LOG.warn("Unsupported device candidate {}", deviceCandidate);
copyDetailsToClipboard(deviceCandidate);
return;
}
stopDiscovery();
final DeviceCoordinator coordinator = deviceType.getDeviceCoordinator();
2023-08-31 23:23:10 +02:00
LOG.info("Using device candidate {} with coordinator {}", deviceCandidate, coordinator.getClass());
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress());
final String authKey = sharedPrefs.getString("authkey", null);
if (authKey == null || authKey.isEmpty()) {
toast(DiscoveryActivityV2.this, getString(R.string.discovery_need_to_enter_authkey), Toast.LENGTH_LONG, GB.WARN);
return;
} else if (!coordinator.validateAuthKey(authKey)) {
2023-08-31 23:23:10 +02:00
toast(DiscoveryActivityV2.this, getString(R.string.discovery_entered_invalid_authkey), Toast.LENGTH_LONG, GB.WARN);
return;
}
}
if (coordinator.suggestUnbindBeforePair() && deviceCandidate.isBonded()) {
new MaterialAlertDialogBuilder(getContext())
.setTitle(R.string.unbind_before_pair_title)
.setMessage(R.string.unbind_before_pair_message)
.setIcon(R.drawable.ic_warning_gray)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
startPair(deviceCandidate, coordinator);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
startPair(deviceCandidate, coordinator);
}
}
private void startPair(final GBDeviceCandidate deviceCandidate, final DeviceCoordinator coordinator) {
2023-08-31 23:23:10 +02:00
final Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
final Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
} else {
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE ||
coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_LAZY) {
LOG.info("No bonding needed, according to coordinator, so connecting right away");
BondingUtil.connectThenComplete(this, deviceCandidate);
return;
}
try {
this.deviceTarget = deviceCandidate;
BondingUtil.initiateCorrectBonding(this, deviceCandidate, coordinator);
2023-08-31 23:23:10 +02:00
} catch (final Exception e) {
LOG.error("Error pairing device {}", deviceCandidate.getMacAddress(), e);
}
}
}
private void copyDetailsToClipboard(final GBDeviceCandidate deviceCandidate) {
final List<String> deviceDetails = new ArrayList<>();
deviceDetails.add(deviceCandidate.getName());
deviceDetails.add(deviceCandidate.getMacAddress());
try {
for (final ParcelUuid uuid : deviceCandidate.getServiceUuids()) {
deviceDetails.add(uuid.getUuid().toString());
}
} catch (final Exception e) {
LOG.error("Error collecting device uuids", e);
}
final String clipboardData = TextUtils.join(", ", deviceDetails);
final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
final ClipData clip = ClipData.newPlainText(deviceCandidate.getName(), clipboardData);
clipboard.setPrimaryClip(clip);
toast(this, "Device details copied to clipboard", Toast.LENGTH_SHORT, GB.INFO);
}
@Override
public boolean onItemLongClick(final AdapterView<?> adapterView, final View view, final int position, final long id) {
stopDiscovery();
final GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
LOG.error("Device candidate clicked, but item not found");
return true;
}
DeviceType deviceType = DeviceHelper.getInstance().resolveDeviceType(deviceCandidate);
if (!deviceType.isSupported()) {
2023-08-31 23:23:10 +02:00
showUnsupportedDeviceDialog(deviceCandidate);
return true;
}
final DeviceCoordinator coordinator = deviceType.getDeviceCoordinator();
2023-08-31 23:23:10 +02:00
final GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (coordinator.getSupportedDeviceSpecificSettings(device) == null) {
return true;
}
final Intent startIntent;
startIntent = new Intent(this, DeviceSettingsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
startIntent.putExtra(DeviceSettingsActivity.MENU_ENTRY_POINT, DeviceSettingsActivity.MENU_ENTRY_POINTS.AUTH_SETTINGS);
} else {
startIntent.putExtra(DeviceSettingsActivity.MENU_ENTRY_POINT, DeviceSettingsActivity.MENU_ENTRY_POINTS.DEVICE_SETTINGS);
}
startActivity(startIntent);
return true;
}
private void showUnsupportedDeviceDialog(final GBDeviceCandidate deviceCandidate) {
LOG.info("Unsupported device candidate selected: {}", deviceCandidate);
final Map<String, Pair<Long, Integer>> allDevices = DebugActivity.getAllSupportedDevices(getApplicationContext());
final LinearLayout linearLayout = new LinearLayout(DiscoveryActivityV2.this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
final ArrayList<SpinnerWithIconItem> deviceListArray = new ArrayList<>();
for (Map.Entry<String, Pair<Long, Integer>> item : allDevices.entrySet()) {
deviceListArray.add(new SpinnerWithIconItem(item.getKey(), item.getValue().first, item.getValue().second));
}
final SpinnerWithIconAdapter deviceListAdapter = new SpinnerWithIconAdapter(
DiscoveryActivityV2.this,
R.layout.spinner_with_image_layout,
R.id.spinner_item_text,
deviceListArray
);
final Spinner deviceListSpinner = new Spinner(DiscoveryActivityV2.this);
deviceListSpinner.setAdapter(deviceListAdapter);
deviceListSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, final long id) {
final SpinnerWithIconItem selectedItem = (SpinnerWithIconItem) parent.getItemAtPosition(pos);
selectedUnsupportedDeviceKey = selectedItem.getId();
}
@Override
public void onNothingSelected(final AdapterView<?> arg0) {
}
});
linearLayout.addView(deviceListSpinner);
final LinearLayout macLayout = new LinearLayout(DiscoveryActivityV2.this);
macLayout.setOrientation(LinearLayout.HORIZONTAL);
macLayout.setPadding(20, 0, 20, 0);
linearLayout.addView(macLayout);
new MaterialAlertDialogBuilder(DiscoveryActivityV2.this)
.setCancelable(true)
.setTitle(R.string.add_test_device)
.setView(linearLayout)
.setPositiveButton(R.string.ok, (dialog, which) -> {
if (selectedUnsupportedDeviceKey != DebugActivity.SELECT_DEVICE) {
DebugActivity.createTestDevice(DiscoveryActivityV2.this, selectedUnsupportedDeviceKey, deviceCandidate.getMacAddress());
finish();
}
})
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
})
.show();
}
@Override
public void onBondingComplete(final boolean success) {
finish();
}
@Override
public GBDeviceCandidate getCurrentTarget() {
return this.deviceTarget;
}
@Override
public String getMacAddress() {
return deviceTarget.getDevice().getAddress();
}
@Override
public boolean getAttemptToConnect() {
return true;
}
2023-08-31 23:23:10 +02:00
@Override
public void registerBroadcastReceivers() {
final IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(bluetoothReceiver, bluetoothIntents);
}
@Override
public void unregisterBroadcastReceivers() {
AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver);
}
@Override
public Context getContext() {
return this;
}
private void loadSettings() {
final Prefs prefs = GBApplication.getPrefs();
deviceFoundProcessor.setDiscoverUnsupported(prefs.getBoolean("discover_unsupported_devices", false));
}
@Override
public void onDeviceChanged() {
refreshDeviceList(true);
}
private final class BluetoothReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
switch (Objects.requireNonNull(intent.getAction())) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED: {
LOG.debug("ACTION_DISCOVERY_STARTED");
break;
}
case BluetoothAdapter.ACTION_STATE_CHANGED: {
LOG.debug("ACTION_STATE_CHANGED ");
bluetoothStateChanged(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF));
break;
}
case BluetoothDevice.ACTION_FOUND: {
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
LOG.warn("ACTION_FOUND with null device");
return;
}
LOG.debug("ACTION_FOUND {}", device.getAddress());
final short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, null));
break;
}
case BluetoothDevice.ACTION_UUID: {
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
LOG.warn("ACTION_UUID with null device");
return;
}
LOG.debug("ACTION_UUID {}", device.getAddress());
final short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
final Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
final ParcelUuid[] uuids2 = AndroidUtils.toParcelUuids(uuids);
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, uuids2));
break;
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
LOG.debug("ACTION_BOND_STATE_CHANGED");
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
LOG.debug("Bond state: {}", bondState);
if (bondState == BluetoothDevice.BOND_BONDED) {
BondingUtil.handleDeviceBonded((BondingInterface) context, getCandidateFromMAC(device));
}
}
break;
}
}
}
}
private final class BleScanCallback extends ScanCallback {
@Override
public void onScanResult(final int callbackType, final ScanResult result) {
super.onScanResult(callbackType, result);
try {
final ScanRecord scanRecord = result.getScanRecord();
ParcelUuid[] uuids = null;
if (scanRecord != null) {
final List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids != null) {
uuids = serviceUuids.toArray(new ParcelUuid[0]);
}
}
final BluetoothDevice device = result.getDevice();
final short rssi = (short) result.getRssi();
LOG.debug("BLE result: {}, {}, {}", device.getAddress(), ((scanRecord != null) ? scanRecord.getBytes().length : -1), rssi);
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, uuids));
} catch (final Exception e) {
LOG.warn("Error handling BLE scan result", e);
}
}
}
}