1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-04 01:09:47 +01:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java
Andreas Shimokawa db48707764 Add a device specific settings activity, currently used for setting the auth key for Amazfit Bip/Cor
When connecting to a new device, a random key gets generated, which can be
looked up from the device specific settings (accessible via the gear icon in
the device card in the main activity). Old devices keep their 0123456789@ABCDE
key, they have to be re-paired to change that.
During pairing, long-pressing the device candidate in the discovery activity
will also start the device specific settings activity, where the auth key
can be set manually priror to pairing. This is usefull to keep the ability to
pair one device with multiple android devices.

Fixes #1308
2019-04-21 21:25:33 +02:00

660 lines
26 KiB
Java

/* Copyright (C) 2015-2019 Andreas Shimokawa, boun, Carsten Pfeiffer,
Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe, Uwe Hermann
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
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.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
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.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
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.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class);
private static final long SCAN_DURATION = 60000; // 60s
private ScanCallback newLeScanCallback = null;
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (Objects.requireNonNull(intent.getAction())) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
discoveryStarted(Scanning.SCANNING_BT);
}
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
handler.post(new Runnable() {
@Override
public void run() {
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
if (GBApplication.isRunningLollipopOrLater()) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
}
} else {
discoveryFinished();
}
}
});
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
bluetoothStateChanged(newState);
break;
case BluetoothDevice.ACTION_FOUND: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
handleDeviceFound(device, rssi);
break;
}
case BluetoothDevice.ACTION_UUID: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
ParcelUuid[] uuids2 = AndroidUtils.toParcelUuids(uuids);
handleDeviceFound(device, rssi, uuids2);
break;
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && bondingDevice != null && device.getAddress().equals(bondingDevice.getMacAddress())) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
handleDeviceBonded();
}
}
}
}
}
};
private void connectAndFinish(GBDevice device) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_trying_to_connect_to, device.getName()), Toast.LENGTH_SHORT, GB.INFO);
GBApplication.deviceService().connect(device, true);
finish();
}
private void createBond(final GBDeviceCandidate deviceCandidate, int bondingStyle) {
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
return;
}
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_ASK) {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(DiscoveryActivity.this.getString(R.string.discovery_pair_title, deviceCandidate.getName()))
.setMessage(DiscoveryActivity.this.getString(R.string.discovery_pair_question))
.setPositiveButton(DiscoveryActivity.this.getString(R.string.discovery_yes_pair), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
doCreatePair(deviceCandidate);
}
})
.setNegativeButton(R.string.discovery_dont_pair, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
connectAndFinish(device);
}
})
.show();
} else {
doCreatePair(deviceCandidate);
}
}
private void doCreatePair(GBDeviceCandidate deviceCandidate) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_attempting_to_pair, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO);
if (deviceCandidate.getDevice().createBond()) {
// async, wait for bonding event to finish this activity
LOG.info("Bonding in progress...");
bondingDevice = deviceCandidate;
} else {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_bonding_failed_immediately, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void handleDeviceBonded() {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_successfully_bonded, bondingDevice.getName()), Toast.LENGTH_SHORT, GB.INFO);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(bondingDevice);
connectAndFinish(device);
}
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
// why use a method to get callback?
// because this callback need API >= 21
// we cant add @TARGETAPI("Lollipop") at class header
// so use a method with SDK check to return this callback
private ScanCallback getScanCallback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
newLeScanCallback = new ScanCallback() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
try {
ScanRecord scanRecord = result.getScanRecord();
ParcelUuid[] uuids = null;
if (scanRecord != null) {
//logMessageContent(scanRecord.getBytes());
List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids != null) {
uuids = serviceUuids.toArray(new ParcelUuid[0]);
}
}
LOG.warn(result.getDevice().getName() + ": " +
((scanRecord != null) ? scanRecord.getBytes().length : -1));
handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
} catch (NullPointerException e) {
LOG.warn("Error handling scan result", e);
}
}
};
}
return newLeScanCallback;
}
public void logMessageContent(byte[] value) {
if (value != null) {
LOG.warn("DATA: " + GB.hexdump(value, 0, value.length));
}
}
private final Runnable stopRunnable = new Runnable() {
@Override
public void run() {
stopDiscovery();
}
};
private ProgressBar progressView;
private BluetoothAdapter adapter;
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
private DeviceCandidateAdapter cadidateListAdapter;
private Button startButton;
private Scanning isScanning = Scanning.SCANNING_OFF;
private GBDeviceCandidate bondingDevice;
private enum Scanning {
SCANNING_BT,
SCANNING_BTLE,
SCANNING_NEW_BTLE,
SCANNING_OFF
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_discovery);
startButton = findViewById(R.id.discovery_start);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onStartButtonClick(startButton);
}
});
progressView = findViewById(R.id.discovery_progressbar);
progressView.setProgress(0);
progressView.setIndeterminate(true);
progressView.setVisibility(View.GONE);
ListView deviceCandidatesView = findViewById(R.id.discovery_deviceCandidatesView);
cadidateListAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
deviceCandidatesView.setAdapter(cadidateListAdapter);
deviceCandidatesView.setOnItemClickListener(this);
deviceCandidatesView.setOnItemLongClickListener(this);
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_DISCOVERY_FINISHED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(bluetoothReceiver, bluetoothIntents);
startDiscovery();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
ArrayList<Parcelable> restoredCandidates = savedInstanceState.getParcelableArrayList("deviceCandidates");
if (restoredCandidates != null) {
deviceCandidates.clear();
for (Parcelable p : restoredCandidates) {
deviceCandidates.add((GBDeviceCandidate) p);
}
}
}
public void onStartButtonClick(View button) {
LOG.debug("Start Button clicked");
if (isScanning()) {
stopDiscovery();
} else {
startDiscovery();
}
}
@Override
protected void onDestroy() {
try {
unregisterReceiver(bluetoothReceiver);
} catch (IllegalArgumentException e) {
LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered.");
}
super.onDestroy();
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
ParcelUuid[] uuids = device.getUuids();
if (uuids == null) {
if (device.fetchUuidsWithSdp()) {
return;
}
}
handleDeviceFound(device, rssi, uuids);
}
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
if (LOG.isDebugEnabled()) {
if (uuids != null && uuids.length > 0) {
for (ParcelUuid uuid : uuids) {
LOG.debug(" supports uuid: " + uuid.toString());
}
}
}
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
return; // ignore already bonded devices
}
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
if (deviceType.isSupported()) {
candidate.setDeviceType(deviceType);
LOG.info("Recognized supported device: " + candidate);
int index = deviceCandidates.indexOf(candidate);
if (index >= 0) {
deviceCandidates.set(index, candidate); // replace
} else {
deviceCandidates.add(candidate);
}
cadidateListAdapter.notifyDataSetChanged();
}
}
/**
* Pre: bluetooth is available, enabled and scanning is off.
* Post: BT is discovering
*/
private void startDiscovery() {
if (isScanning()) {
LOG.warn("Not starting discovery, because already scanning.");
return;
}
startDiscovery(Scanning.SCANNING_BT);
}
private void startDiscovery(Scanning what) {
LOG.info("Starting discovery: " + what);
discoveryStarted(what); // just to make sure
if (ensureBluetoothReady()) {
if (what == Scanning.SCANNING_BT) {
startBTDiscovery();
} else if (what == Scanning.SCANNING_BTLE) {
if (GB.supportsBluetoothLE()) {
startBTLEDiscovery();
} else {
discoveryFinished();
}
} else if (what == Scanning.SCANNING_NEW_BTLE) {
if (GB.supportsBluetoothLE()) {
startNEWBTLEDiscovery();
} else {
discoveryFinished();
}
}
} else {
discoveryFinished();
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
}
}
private boolean isScanning() {
return isScanning != Scanning.SCANNING_OFF;
}
private void stopDiscovery() {
LOG.info("Stopping discovery");
if (isScanning()) {
Scanning wasScanning = isScanning;
// unfortunately, we don't always get a call back when stopping the scan, so
// we do it manually; BEFORE stopping the scan!
discoveryFinished();
if (wasScanning == Scanning.SCANNING_BT) {
stopBTDiscovery();
} else if (wasScanning == Scanning.SCANNING_BTLE) {
stopBTLEDiscovery();
} else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
stopNewBTLEDiscovery();
}
handler.removeMessages(0, stopRunnable);
}
}
private void stopBTLEDiscovery() {
if (adapter != null)
adapter.stopLeScan(leScanCallback);
}
private void stopBTDiscovery() {
if (adapter != null)
adapter.cancelDiscovery();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void stopNewBTLEDiscovery() {
if (adapter == null)
return;
BluetoothLeScanner bluetoothLeScanner = adapter.getBluetoothLeScanner();
if (bluetoothLeScanner == null) {
LOG.warn("could not get BluetoothLeScanner()!");
return;
}
if (newLeScanCallback == null) {
LOG.warn("newLeScanCallback == null!");
return;
}
bluetoothLeScanner.stopScan(newLeScanCallback);
}
private void bluetoothStateChanged(int newState) {
discoveryFinished();
if (newState == BluetoothAdapter.STATE_ON) {
this.adapter = BluetoothAdapter.getDefaultAdapter();
startButton.setEnabled(true);
} else {
this.adapter = null;
startButton.setEnabled(false);
}
}
private void discoveryFinished() {
isScanning = Scanning.SCANNING_OFF;
progressView.setVisibility(View.GONE);
startButton.setText(getString(R.string.discovery_start_scanning));
}
private void discoveryStarted(Scanning what) {
isScanning = what;
progressView.setVisibility(View.VISIBLE);
startButton.setText(getString(R.string.discovery_stop_scanning));
}
private boolean ensureBluetoothReady() {
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 boolean checkBluetoothAvailable() {
BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
if (bluetoothService == null) {
LOG.warn("No bluetooth available");
this.adapter = null;
return false;
}
BluetoothAdapter adapter = bluetoothService.getAdapter();
if (adapter == null) {
LOG.warn("No bluetooth available");
this.adapter = null;
return false;
}
if (!adapter.isEnabled()) {
LOG.warn("Bluetooth not enabled");
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableBtIntent);
this.adapter = null;
return false;
}
this.adapter = adapter;
return true;
}
// New BTLE Discovery use startScan (List<ScanFilter> filters,
// ScanSettings settings,
// ScanCallback callback)
// It's added on API21
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startNEWBTLEDiscovery() {
// Only use new API when user uses Lollipop+ device
LOG.info("Start New BTLE Discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
adapter.getBluetoothLeScanner().startScan(getScanFilters(), getScanSettings(), getScanCallback());
}
private List<ScanFilter> getScanFilters() {
List<ScanFilter> allFilters = new ArrayList<>();
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
allFilters.addAll(coordinator.createBLEScanFilters());
}
return allFilters;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ScanSettings getScanSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return new ScanSettings.Builder()
.setScanMode(android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY)
.build();
} else {
return new ScanSettings.Builder()
.setScanMode(android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
}
}
private void startBTLEDiscovery() {
LOG.info("Starting BTLE Discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
adapter.startLeScan(leScanCallback);
}
private void startBTDiscovery() {
LOG.info("Starting BT Discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
adapter.startDiscovery();
}
private void checkAndRequestLocationPermission() {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0);
}
}
private Message getPostMessage(Runnable runnable) {
Message m = Message.obtain(handler, runnable);
m.obj = runnable;
return m;
}
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id) {
GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
LOG.error("Device candidate clicked, but item not found");
return true;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (!coordinator.supportsDeviceSpecificSettings(device)) {
return true;
}
Intent startIntent;
startIntent = new Intent(this, DeviceSettingsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
startActivity(startIntent);
return true;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
if (deviceCandidate == null) {
LOG.error("Device candidate clicked, but item not found");
return;
}
stopDiscovery();
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
} else {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
int bondingStyle = coordinator.getBondingStyle(device);
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("No bonding needed, according to coordinator, so connecting right away");
connectAndFinish(device);
return;
}
try {
BluetoothDevice btDevice = adapter.getRemoteDevice(deviceCandidate.getMacAddress());
switch (btDevice.getBondState()) {
case BluetoothDevice.BOND_NONE: {
createBond(deviceCandidate, bondingStyle);
break;
}
case BluetoothDevice.BOND_BONDING:
// async, wait for bonding event to finish this activity
bondingDevice = deviceCandidate;
break;
case BluetoothDevice.BOND_BONDED:
handleDeviceBonded();
break;
}
} catch (Exception e) {
LOG.error("Error pairing device: " + deviceCandidate.getMacAddress());
}
}
}
@Override
protected void onPause() {
super.onPause();
stopBTDiscovery();
stopBTLEDiscovery();
if (GBApplication.isRunningLollipopOrLater()) {
stopNewBTLEDiscovery();
}
}
}