1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-19 11:30:44 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java
José Rebelo 8085eb6ec3 Discover bonded devices by default
A lof of devices will simply work anyway even if already paired in
Android bluetooth settings. Discover them by default, but warn the user
if the device is not known to pair correctly if already paired in Android
settings. Allows this warning to be disabled to known working devices.
2024-03-25 20:31:33 +00:00

888 lines
37 KiB
Java

/* Copyright (C) 2023-2024 Andreas Böhler, Daniel Dakhno, José Rebelo
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/>. */
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;
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;
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);
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());
}
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()) {
LOG.warn("Unsupported device candidate {}", deviceCandidate);
copyDetailsToClipboard(deviceCandidate);
return;
}
stopDiscovery();
final DeviceCoordinator coordinator = deviceType.getDeviceCoordinator();
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)) {
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) {
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);
} 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()) {
showUnsupportedDeviceDialog(deviceCandidate);
return true;
}
final DeviceCoordinator coordinator = deviceType.getDeviceCoordinator();
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;
}
@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);
}
}
}
}