diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index 88058739af..8f60fb9395 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -20,7 +20,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities; import android.Manifest; import android.app.Activity; -import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; @@ -30,15 +29,10 @@ import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanRecord; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; -import android.companion.AssociationRequest; -import android.companion.BluetoothDeviceFilter; -import android.companion.CompanionDeviceManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.location.LocationManager; @@ -65,6 +59,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -76,6 +71,8 @@ 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; @@ -83,10 +80,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast; -public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { +public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener, BondingInterface { private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class); private static final long SCAN_DURATION = 30000; // 30s - private static final int REQUEST_CODE = 1; private final Handler handler = new Handler(); private final ArrayList deviceCandidates = new ArrayList<>(); private ScanCallback newBLEScanCallback = null; @@ -98,10 +94,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView * If already bonded devices are to be ignored when scanning */ private boolean ignoreBonded = true; - /** - * If new CompanionDevice-type pairing is enabled on newer Androids - **/ - private boolean enableCompanionDevicePairing = false; private ProgressBar bluetoothProgress; private ProgressBar bluetoothLEProgress; private DeviceCandidateAdapter deviceCandidateAdapter; @@ -127,7 +119,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } } }; - private GBDeviceCandidate bondingDevice; private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -146,7 +137,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView handler.post(new Runnable() { @Override public void run() { - // continue with LE scan, if available + // Continue with LE scan, if available if (isScanning == Scanning.SCANNING_BT || isScanning == Scanning.SCANNING_BT_NEXT_BLE) { checkAndRequestLocationPermission(); stopDiscovery(); @@ -158,15 +149,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } case BluetoothAdapter.ACTION_STATE_CHANGED: { LOG.debug("ACTION_STATE_CHANGED "); - int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - bluetoothStateChanged(newState); + bluetoothStateChanged(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF)); break; } case BluetoothDevice.ACTION_FOUND: { LOG.debug("ACTION_FOUND"); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN); - handleDeviceFound(device, rssi); + handleDeviceFound(device, intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN)); break; } case BluetoothDevice.ACTION_UUID: { @@ -181,70 +170,20 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView case BluetoothDevice.ACTION_BOND_STATE_CHANGED: { LOG.debug("ACTION_BOND_STATE_CHANGED"); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device != null && bondingDevice != null && device.getAddress().equals(bondingDevice.getMacAddress())) { + if (device != null) { int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); + LOG.debug(String.format(Locale.ENGLISH, "Bond state: %d", bondState)); + if (bondState == BluetoothDevice.BOND_BONDED) { - handleDeviceBonded(); + BondingUtil.handleDeviceBonded((BondingInterface) context, getCandidateFromMAC(device)); } } + break; } } } }; - - private void connectAndFinish(GBDevice device) { - 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) { - // Do nothing - return; - } else 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); - } - LOG.debug("Bonding initiated"); - } - - private void doCreatePair(GBDeviceCandidate deviceCandidate) { - toast(DiscoveryActivity.this, getString(R.string.discovery_attempting_to_pair, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO); - if (enableCompanionDevicePairing && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - companionDevicePair(deviceCandidate); - } else { - deviceBond(deviceCandidate); - } - } - - private void deviceBond(GBDeviceCandidate deviceCandidate) { - if (deviceCandidate.getDevice().createBond()) { - // Async, wait for bonding event to finish this activity - LOG.info("Bonding in progress..."); - bondingDevice = deviceCandidate; - } else { - toast(DiscoveryActivity.this, getString(R.string.discovery_bonding_failed_immediately, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.ERROR); - } - } + private BluetoothDevice bluetoothTarget; public void logMessageContent(byte[] value) { if (value != null) { @@ -252,66 +191,22 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } } - @RequiresApi(Build.VERSION_CODES.O) - private void companionDevicePair(final GBDeviceCandidate deviceCandidate) { - CompanionDeviceManager deviceManager = getSystemService(CompanionDeviceManager.class); - - BluetoothDeviceFilter deviceFilter = new BluetoothDeviceFilter.Builder() - .setAddress(deviceCandidate.getMacAddress()) - .build(); - - AssociationRequest pairingRequest = new AssociationRequest.Builder() - .addDeviceFilter(deviceFilter) - .setSingleDevice(true) - .build(); - - deviceManager.associate(pairingRequest, - new CompanionDeviceManager.Callback() { - @Override - public void onFailure(CharSequence error) { - toast(DiscoveryActivity.this, getString(R.string.discovery_bonding_failed_immediately, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.ERROR); - } - - @Override - public void onDeviceFound(IntentSender chooserLauncher) { - try { - startIntentSenderForResult(chooserLauncher, - REQUEST_CODE, null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - e.printStackTrace(); - } - } - }, - null - ); - } - @RequiresApi(Build.VERSION_CODES.O) @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE && - resultCode == Activity.RESULT_OK) { - - BluetoothDevice deviceToPair = - data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE); - - if (deviceToPair != null) { - deviceBond(new GBDeviceCandidate(deviceToPair, (short) 0, null)); - handleDeviceBonded(); - } - } + BondingUtil.handleActivityResult(this, requestCode, resultCode, data); } - private void handleDeviceBonded() { - if (bondingDevice == null) { - LOG.error("deviceCandidate was null! Can't handle bonded device!"); - return; - } - 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 GBDeviceCandidate getCandidateFromMAC(BluetoothDevice device) { + for (GBDeviceCandidate candidate : deviceCandidates) { + if (candidate.getMacAddress().equals(device.getAddress())) { + return candidate; + } + } + LOG.warn(String.format("This shouldn't happen unless the list somehow emptied itself, device MAC: %1$s", device.getAddress())); + return null; } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @@ -359,11 +254,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView LOG.info("New BLE scanning disabled via settings, using old method"); } - enableCompanionDevicePairing = prefs.getBoolean("enable_companiondevice_pairing", true); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - enableCompanionDevicePairing = false; // No support below 26 - } - setContentView(R.layout.activity_discovery); startButton = findViewById(R.id.discovery_start); startButton.setOnClickListener(new View.OnClickListener() { @@ -389,15 +279,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView 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); + removeBroadcastReceivers(); checkAndRequestLocationPermission(); @@ -437,16 +319,40 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView @Override protected void onDestroy() { - try { - unregisterReceiver(bluetoothReceiver); - } catch (IllegalArgumentException e) { - LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered"); - LOG.warn(e.getMessage()); - } - + unregisterBroadcastReceivers(); + stopAllDiscovery(); super.onDestroy(); } + @Override + protected void onStop() { + unregisterBroadcastReceivers(); + stopAllDiscovery(); + super.onStop(); + } + + @Override + protected void onPause() { + unregisterBroadcastReceivers(); + stopAllDiscovery(); + super.onPause(); + } + + private void stopAllDiscovery() { + try { + stopBTDiscovery(); + if (oldBleScanning) { + stopOldBLEDiscovery(); + } else { + if (GBApplication.isRunningLollipopOrLater()) { + stopBLEDiscovery(); + } + } + } catch (Exception e) { + LOG.warn("Error stopping discovery", e); + } + } + private void handleDeviceFound(BluetoothDevice device, short rssi) { if (device.getName() != null) { if (handleDeviceFound(device, rssi, null)) { @@ -552,7 +458,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView handler.removeMessages(0, stopRunnable); handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION); - if(adapter.startLeScan(leScanCallback)) { + if (adapter.startLeScan(leScanCallback)) { LOG.info("Old Bluetooth LE scan started successfully"); bluetoothLEProgress.setVisibility(View.VISIBLE); setIsScanning(Scanning.SCANNING_BLE); @@ -791,6 +697,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } catch (Exception ex) { LOG.error("Exception when checking location status: ", ex); } + LOG.error("Problem with permissions, returning"); } @Override @@ -824,32 +731,15 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate); startActivity(intent); } else { - GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); - int bondingStyle = coordinator.getBondingStyle(); - if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) { + if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE) { LOG.info("No bonding needed, according to coordinator, so connecting right away"); - connectAndFinish(device); + BondingUtil.connectThenComplete(this, deviceCandidate); 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: { - bondingDevice = deviceCandidate; - handleDeviceBonded(); - break; - } - } + this.bluetoothTarget = deviceCandidate.getDevice(); + BondingUtil.initiateCorrectBonding(this, deviceCandidate); } catch (Exception e) { LOG.error("Error pairing device: " + deviceCandidate.getMacAddress()); } @@ -877,17 +767,33 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView return true; } + public void onBondingComplete(boolean success) { + finish(); + } + + public BluetoothDevice getCurrentTarget() { + return this.bluetoothTarget; + } + + public void unregisterBroadcastReceivers() { + AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver); + } + + public void removeBroadcastReceivers() { + 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); + } + @Override - protected void onPause() { - super.onPause(); - stopBTDiscovery(); - if (oldBleScanning) { - stopOldBLEDiscovery(); - } else { - if (GBApplication.isRunningLollipopOrLater()) { - stopBLEDiscovery(); - } - } + public Context getContext() { + return this; } private enum Scanning { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 49ee0fa461..9c11996030 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -37,7 +37,6 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; -import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9PairingActivity; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao; @@ -109,9 +108,10 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { /** * Hook for subclasses to perform device-specific deletion logic, e.g. db cleanup. + * * @param gbDevice the GBDevice - * @param device the corresponding database Device - * @param session the session to use + * @param device the corresponding database Device + * @param session the session to use * @throws GBException */ protected abstract void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException; @@ -128,15 +128,15 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } if (bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.WEARABLE - || bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED) { + || bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED) { int deviceClasses = BluetoothClass.Device.HEALTH_BLOOD_PRESSURE - | BluetoothClass.Device.HEALTH_DATA_DISPLAY - | BluetoothClass.Device.HEALTH_PULSE_RATE - | BluetoothClass.Device.HEALTH_WEIGHING - | BluetoothClass.Device.HEALTH_UNCATEGORIZED - | BluetoothClass.Device.HEALTH_PULSE_OXIMETER - | BluetoothClass.Device.HEALTH_GLUCOSE; + | BluetoothClass.Device.HEALTH_DATA_DISPLAY + | BluetoothClass.Device.HEALTH_PULSE_RATE + | BluetoothClass.Device.HEALTH_WEIGHING + | BluetoothClass.Device.HEALTH_UNCATEGORIZED + | BluetoothClass.Device.HEALTH_PULSE_OXIMETER + | BluetoothClass.Device.HEALTH_GLUCOSE; return (bluetoothClass.getDeviceClass() & deviceClasses) != 0; } @@ -185,7 +185,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { } @Override - public boolean supportsUnicodeEmojis() { return false; } + public boolean supportsUnicodeEmojis() { + return false; + } @Override public int[] getSupportedDeviceSpecificSettings(GBDevice device) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java index 06b3016997..92c88e2ccd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian +/* Copyright (C) 2018-2020 Daniele Gobbetti, maxirnilian, Taavi Eomäe This file is part of Gadgetbridge. @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.lenovo; +import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -24,12 +25,12 @@ import android.os.Bundle; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; @@ -38,33 +39,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; -import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface; +import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil; -public class LenovoWatchPairingActivity extends AbstractGBActivity { +import static nodomain.freeyourgadget.gadgetbridge.util.BondingUtil.STATE_DEVICE_CANDIDATE; + +public class LenovoWatchPairingActivity extends AbstractGBActivity implements BondingInterface { private static final Logger LOG = LoggerFactory.getLogger(LenovoWatchPairingActivity.class); - private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; - private TextView message; private GBDeviceCandidate deviceCandidate; - private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { - GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - LOG.debug("pairing activity: device changed: " + device); - if (deviceCandidate.getMacAddress().equals(device.getAddress())) { - if (device.isInitialized()) { - pairingFinished(); - } else if (device.isConnecting() || device.isInitializing()) { - LOG.info("still connecting/initializing device..."); - } - } - } - } - }; + private final BroadcastReceiver pairingReceiver = BondingUtil.getPairingReceiver(this); @Override protected void onCreate(Bundle savedInstanceState) { @@ -83,7 +69,8 @@ public class LenovoWatchPairingActivity extends AbstractGBActivity { finish(); return; } - startPairing(); + + startPairing(deviceCandidate); } @Override @@ -98,33 +85,72 @@ public class LenovoWatchPairingActivity extends AbstractGBActivity { deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + BondingUtil.handleActivityResult(this, requestCode, resultCode, data); + } + + private void startPairing(GBDeviceCandidate deviceCandidate) { + message.setText(getString(R.string.pairing, deviceCandidate)); + + removeBroadcastReceivers(); + + BondingUtil.connectThenComplete(this, deviceCandidate); + } + + @Override + public void onBondingComplete(boolean success) { + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + finish(); + } + + @Override + public BluetoothDevice getCurrentTarget() { + return this.deviceCandidate.getDevice(); + } + + @Override + protected void onResume() { + removeBroadcastReceivers(); + super.onResume(); + } + + @Override + protected void onStart() { + removeBroadcastReceivers(); + super.onStart(); + } + @Override protected void onDestroy() { - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + unregisterBroadcastReceivers(); super.onDestroy(); } - private void startPairing() { - message.setText(getString(R.string.pairing, deviceCandidate)); - - IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); - LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); - - GBApplication.deviceService().disconnect(); - GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); - if (device != null) { - GBApplication.deviceService().connect(device, true); - } else { - GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR); - } + @Override + protected void onStop() { + unregisterBroadcastReceivers(); + super.onStop(); } - private void pairingFinished() { - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + @Override + protected void onPause() { + // WARN: Do not remove listeners on pause + // Bonding process can pause the activity and you might miss broadcasts + super.onPause(); + } - Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); + public void unregisterBroadcastReceivers() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), pairingReceiver); + } - finish(); + public void removeBroadcastReceivers() { + LocalBroadcastManager.getInstance(this).registerReceiver(pairingReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); + } + + @Override + public Context getContext() { + return this; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java index c365f676ac..b9a613bb87 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, Taavi Eomäe This file is part of Gadgetbridge. @@ -25,8 +25,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.widget.TextView; import android.widget.Toast; @@ -45,74 +43,24 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; 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 MiBandPairingActivity extends AbstractGBActivity { +import static nodomain.freeyourgadget.gadgetbridge.util.BondingUtil.STATE_DEVICE_CANDIDATE; + +public class MiBandPairingActivity extends AbstractGBActivity implements BondingInterface { private static final Logger LOG = LoggerFactory.getLogger(MiBandPairingActivity.class); private static final int REQ_CODE_USER_SETTINGS = 52; - private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; - private static final long DELAY_AFTER_BONDING = 1000; // 1s + + private final BroadcastReceiver pairingReceiver = BondingUtil.getPairingReceiver(this); + private final BroadcastReceiver bondingReceiver = BondingUtil.getBondingReceiver(this); private TextView message; private boolean isPairing; private GBDeviceCandidate deviceCandidate; - private String bondingMacAddress; - - private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { - GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - LOG.debug("pairing activity: device changed: " + device); - if (deviceCandidate.getMacAddress().equals(device.getAddress())) { - if (device.isInitialized()) { - pairingFinished(true, deviceCandidate); - } else if (device.isConnecting() || device.isInitializing()) { - LOG.info("still connecting/initializing device..."); - } - } - } - } - }; - - private final BroadcastReceiver mBondingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + bondingMacAddress); - if (bondingMacAddress != null && bondingMacAddress.equals(device.getAddress())) { - int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - if (bondState == BluetoothDevice.BOND_BONDED) { - LOG.info("Bonded with " + device.getAddress()); - bondingMacAddress = null; - attemptToConnect(); - } else if (bondState == BluetoothDevice.BOND_BONDING) { - LOG.info("Bonding in progress with " + device.getAddress()); - } else if (bondState == BluetoothDevice.BOND_NONE) { - LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway."); - bondingMacAddress = null; - attemptToConnect(); - } else { - LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState); - pairingFinished(false, deviceCandidate); - } - } - } - } - }; - - private void attemptToConnect() { - Looper mainLooper = Looper.getMainLooper(); - new Handler(mainLooper).postDelayed(new Runnable() { - @Override - public void run() { - performApplicationLevelPair(); - } - }, DELAY_AFTER_BONDING); - } @Override protected void onCreate(Bundle savedInstanceState) { @@ -120,11 +68,11 @@ public class MiBandPairingActivity extends AbstractGBActivity { setContentView(R.layout.activity_mi_band_pairing); message = findViewById(R.id.miband_pair_message); - Intent intent = getIntent(); - deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + this.deviceCandidate = getIntent().getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); if (deviceCandidate == null && savedInstanceState != null) { - deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + this.deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); } + if (deviceCandidate == null) { Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show(); startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); @@ -179,56 +127,40 @@ public class MiBandPairingActivity extends AbstractGBActivity { } startPairing(); } + + BondingUtil.handleActivityResult(this, requestCode, resultCode, data); } - @Override - protected void onDestroy() { - // just to be sure, remove the receivers -- might actually be already unregistered - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); - AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver); - if (isPairing) { - stopPairing(); - } - super.onDestroy(); - } private void startPairing() { isPairing = true; message.setText(getString(R.string.pairing, deviceCandidate)); - IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); - LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); - - if (!shouldSetupBTLevelPairing()) { - // there are connection problems on certain Galaxy S devices at least; - // try to connect without BT pairing (bonding) - attemptToConnect(); + if (!BondingUtil.shouldUseBonding()) { + BondingUtil.attemptToFirstConnect(getCurrentTarget()); return; } - filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - registerReceiver(mBondingReceiver, filter); - - performBluetoothPair(deviceCandidate); + BondingUtil.tryBondThenComplete(this, deviceCandidate); } - private boolean shouldSetupBTLevelPairing() { - Prefs prefs = GBApplication.getPrefs(); - return prefs.getPreferences().getBoolean(MiBandConst.PREF_MIBAND_SETUP_BT_PAIRING, true); + + private void stopPairing() { + isPairing = false; + BondingUtil.stopBluetoothBonding(deviceCandidate.getDevice()); } - private void pairingFinished(boolean pairedSuccessfully, GBDeviceCandidate candidate) { - LOG.debug("pairingFinished: " + pairedSuccessfully); + @Override + public void onBondingComplete(boolean success) { + LOG.debug("pairingFinished: " + success); if (!isPairing) { // already gone? return; + } else { + isPairing = false; } - isPairing = false; - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); - AndroidUtils.safeUnregisterBroadcastReceiver(this, mBondingReceiver); - - if (pairedSuccessfully) { + if (success) { // remember the device since we do not necessarily pair... temporary -- we probably need // to query the db for available devices in ControlCenter. But only remember un-bonded // devices, as bonded devices are displayed anyway. @@ -244,40 +176,59 @@ public class MiBandPairingActivity extends AbstractGBActivity { finish(); } - private void stopPairing() { - // TODO - isPairing = false; + @Override + public BluetoothDevice getCurrentTarget() { + return this.deviceCandidate.getDevice(); } - protected void performBluetoothPair(GBDeviceCandidate deviceCandidate) { - BluetoothDevice device = deviceCandidate.getDevice(); - - int bondState = device.getBondState(); - if (bondState == BluetoothDevice.BOND_BONDED) { - GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO); - performApplicationLevelPair(); - return; - } - - bondingMacAddress = device.getAddress(); - if (bondState == BluetoothDevice.BOND_BONDING) { - GB.toast(this, getString(R.string.pairing_in_progress, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO); - return; - } - - GB.toast(this, getString(R.string.pairing_creating_bond_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO); - if (!device.createBond()) { - GB.toast(this, getString(R.string.pairing_unable_to_pair_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.ERROR); - } + @Override + protected void onResume() { + removeBroadcastReceivers(); + super.onResume(); } - private void performApplicationLevelPair() { - GBApplication.deviceService().disconnect(); // just to make sure... - GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); - if (device != null) { - GBApplication.deviceService().connect(device, true); - } else { - GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR); + @Override + protected void onStart() { + removeBroadcastReceivers(); + super.onStart(); + } + + @Override + protected void onDestroy() { + unregisterBroadcastReceivers(); + if (isPairing) { + stopPairing(); } + super.onDestroy(); + } + + @Override + protected void onStop() { + unregisterBroadcastReceivers(); + if (isPairing) { + stopPairing(); + } + super.onStop(); + } + + @Override + protected void onPause() { + // WARN: Do not stop pairing or unregister receivers pause! + // Bonding process can pause the activity and you might miss broadcasts + super.onPause(); + } + + public void unregisterBroadcastReceivers() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), pairingReceiver); + AndroidUtils.safeUnregisterBroadcastReceiver(this, bondingReceiver); + } + + public void removeBroadcastReceivers() { + LocalBroadcastManager.getInstance(this).registerReceiver(pairingReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); + registerReceiver(bondingReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); + } + + public Context getContext() { + return this; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java index 9953871884..a20d7ebb82 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, Taavi Eomäe This file is part of Gadgetbridge. @@ -27,6 +27,7 @@ import android.os.Bundle; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; @@ -47,218 +48,210 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao; 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; -public class PebblePairingActivity extends AbstractGBActivity { +import static nodomain.freeyourgadget.gadgetbridge.util.BondingUtil.STATE_DEVICE_CANDIDATE; + + +public class PebblePairingActivity extends AbstractGBActivity implements BondingInterface { private static final Logger LOG = LoggerFactory.getLogger(PebblePairingActivity.class); + private final BroadcastReceiver pairingReceiver = BondingUtil.getPairingReceiver(this); + private final BroadcastReceiver bondingReceiver = BondingUtil.getBondingReceiver(this); + private TextView message; private boolean isPairing; - private boolean isLEPebble; - private String macAddress; - private BluetoothDevice mBtDevice; - private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { - GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - LOG.debug("pairing activity: device changed: " + device); - if (macAddress.equals(device.getAddress()) || macAddress.equals(device.getVolatileAddress())) { - if (device.isInitialized()) { - pairingFinished(true); - } else if (device.isConnecting() || device.isInitializing()) { - LOG.info("still connecting/initializing device..."); - } - } - } - } - }; - - private final BroadcastReceiver mBondingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + macAddress); - if (macAddress != null && macAddress.equals(device.getAddress())) { - int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - if (bondState == BluetoothDevice.BOND_BONDED) { - LOG.info("Bonded with " + device.getAddress()); - if (!isLEPebble) { - performConnect(null); - } - } else if (bondState == BluetoothDevice.BOND_BONDING) { - LOG.info("Bonding in progress with " + device.getAddress()); - } else if (bondState == BluetoothDevice.BOND_NONE) { - LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway."); - } else { - LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState); - pairingFinished(false); - } - } - } - } - }; + private GBDeviceCandidate deviceCandidate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pebble_pairing); - message = (TextView) findViewById(R.id.pebble_pair_message); - Intent intent = getIntent(); - GBDeviceCandidate candidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); - if (candidate != null) { - macAddress = candidate.getMacAddress(); + message = findViewById(R.id.pebble_pair_message); + deviceCandidate = getIntent().getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + + String macAddress = null; + if (deviceCandidate != null) { + macAddress = deviceCandidate.getMacAddress(); } + if (macAddress == null) { Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show(); - returnToPairingActivity(); + onBondingComplete(false); return; } - mBtDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress); - if (mBtDevice == null) { + BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress); + if (btDevice == null) { GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR); - returnToPairingActivity(); + onBondingComplete(false); return; } - isLEPebble = mBtDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE; - - GBDevice gbDevice = null; - if (isLEPebble) { - if (mBtDevice.getName().startsWith("Pebble-LE ") || mBtDevice.getName().startsWith("Pebble Time LE ")) { - if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) { - GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR); - returnToPairingActivity(); - return; - } - gbDevice = getMatchingParentDeviceFromDB(mBtDevice); - if (gbDevice == null) { - return; - } - } - } - startPairing(gbDevice); + startBonding(btDevice); } - @Override - protected void onDestroy() { - try { - // just to be sure, remove the receivers -- might actually be already unregistered - LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver); - unregisterReceiver(mBondingReceiver); - } catch (IllegalArgumentException ex) { - // already unregistered, ignore - } - if (isPairing) { - stopPairing(); - } - - super.onDestroy(); - } - - private void startPairing(GBDevice gbDevice) { + private void startBonding(BluetoothDevice btDevice) { isPairing = true; - message.setText(getString(R.string.pairing, macAddress)); + message.setText(getString(R.string.pairing, btDevice.getAddress())); - IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); - LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); - filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - registerReceiver(mBondingReceiver, filter); + GBDevice device; + if (BondingUtil.isLePebble(btDevice)) { + if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) { + GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR); + onBondingComplete(false); + return; + } - performPair(gbDevice); - } + device = getMatchingParentDeviceFromDBAndSetVolatileAddress(btDevice); + if (device == null) { + onBondingComplete(false); + return; + } - private void pairingFinished(boolean pairedSuccessfully) { - LOG.debug("pairingFinished: " + pairedSuccessfully); - if (!isPairing) { - // already gone? + removeBroadcastReceivers(); + BondingUtil.connectThenComplete(this, device); return; } - isPairing = false; - LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver); - unregisterReceiver(mBondingReceiver); - - if (pairedSuccessfully) { - Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - } - finish(); - } - - private void stopPairing() { - // TODO - isPairing = false; - } - - protected void performPair(GBDevice gbDevice) { - int bondState = mBtDevice.getBondState(); - if (bondState == BluetoothDevice.BOND_BONDED) { - GB.toast(getString(R.string.pairing_already_bonded, mBtDevice.getName(), mBtDevice.getAddress()), Toast.LENGTH_SHORT, GB.INFO); - return; - } - - if (bondState == BluetoothDevice.BOND_BONDING) { - GB.toast(this, getString(R.string.pairing_in_progress, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO); - return; - } - - GB.toast(this, getString(R.string.pairing_creating_bond_with, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO); - GBApplication.deviceService().disconnect(); // just to make sure... - - if (isLEPebble) { - performConnect(gbDevice); + if (btDevice.getBondState() == BluetoothDevice.BOND_BONDED || + btDevice.getBondState() == BluetoothDevice.BOND_BONDING) { + BondingUtil.connectThenComplete(this, deviceCandidate); } else { - mBtDevice.createBond(); + BondingUtil.tryBondThenComplete(this, deviceCandidate); } } - private void performConnect(GBDevice gbDevice) { - if (gbDevice == null) { - gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), null, DeviceType.PEBBLE); - } - GBApplication.deviceService().connect(gbDevice); + private void stopBonding() { + isPairing = false; + BondingUtil.stopBluetoothBonding(deviceCandidate.getDevice()); } - private void returnToPairingActivity() { - startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); - finish(); - } - - private GBDevice getMatchingParentDeviceFromDB(BluetoothDevice btDevice) { + private GBDevice getMatchingParentDeviceFromDBAndSetVolatileAddress(BluetoothDevice btDevice) { String expectedSuffix = btDevice.getName(); expectedSuffix = expectedSuffix.replace("Pebble-LE ", ""); expectedSuffix = expectedSuffix.replace("Pebble Time LE ", ""); expectedSuffix = expectedSuffix.substring(0, 2) + ":" + expectedSuffix.substring(2); - LOG.info("will try to find a Pebble with BT address suffix " + expectedSuffix); - GBDevice gbDevice = null; + LOG.info("Trying to find a Pebble with BT address suffix " + expectedSuffix); try (DBHandler dbHandler = GBApplication.acquireDB()) { DaoSession session = dbHandler.getDaoSession(); DeviceDao deviceDao = session.getDeviceDao(); Query query = deviceDao.queryBuilder().where(DeviceDao.Properties.Type.eq(1), DeviceDao.Properties.Identifier.like("%" + expectedSuffix)).build(); + List devices = query.list(); if (devices.size() == 0) { GB.toast("Please pair your non-LE Pebble before pairing the LE one", Toast.LENGTH_SHORT, GB.INFO); - returnToPairingActivity(); + onBondingComplete(false); return null; } else if (devices.size() > 1) { GB.toast("Can not match this Pebble LE to a unique device", Toast.LENGTH_SHORT, GB.INFO); - returnToPairingActivity(); + onBondingComplete(false); return null; } - DeviceHelper deviceHelper = DeviceHelper.getInstance(); - gbDevice = deviceHelper.toGBDevice(devices.get(0)); + + GBDevice gbDevice = DeviceHelper.getInstance().toGBDevice(devices.get(0)); gbDevice.setVolatileAddress(btDevice.getAddress()); + return gbDevice; } catch (Exception e) { GB.toast(getString(R.string.error_retrieving_devices_database), Toast.LENGTH_SHORT, GB.ERROR, e); - returnToPairingActivity(); + onBondingComplete(false); return null; } - return gbDevice; + } + + @Override + public void onBondingComplete(boolean success) { + unregisterBroadcastReceivers(); + if (success) { + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + } else { + startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + } + + // If it's not a LE Pebble, initiate a connection when bonding is complete + if (!BondingUtil.isLePebble(getCurrentTarget()) && success) { + BondingUtil.attemptToFirstConnect(getCurrentTarget()); + } + finish(); + } + + @Override + public BluetoothDevice getCurrentTarget() { + return this.deviceCandidate.getDevice(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + BondingUtil.handleActivityResult(this, requestCode, resultCode, data); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + } + + @Override + protected void onStart() { + removeBroadcastReceivers(); + super.onStart(); + } + + @Override + protected void onResume() { + removeBroadcastReceivers(); + super.onResume(); + } + + @Override + protected void onDestroy() { + unregisterBroadcastReceivers(); + if (isPairing) { + stopBonding(); + } + super.onDestroy(); + } + + @Override + protected void onStop() { + unregisterBroadcastReceivers(); + if (isPairing) { + stopBonding(); + } + super.onStop(); + } + + @Override + protected void onPause() { + // WARN: Do not stop bonding process during pause! + // Bonding process can pause the activity and you might miss broadcasts + super.onPause(); + } + + public void unregisterBroadcastReceivers() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), pairingReceiver); + AndroidUtils.safeUnregisterBroadcastReceiver(this, bondingReceiver); + } + + public void removeBroadcastReceivers() { + LocalBroadcastManager.getInstance(this).registerReceiver(pairingReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); + registerReceiver(bondingReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); + } + + @Override + public Context getContext() { + return this; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java index 4712d110f4..ea3f1bf6ae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2018-2020 Daniele Gobbetti, maxirnilian +/* Copyright (C) 2018-2020 Daniele Gobbetti, maxirnilian, Taavi Eomäe This file is part of Gadgetbridge. @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.watch9; +import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -24,11 +25,11 @@ import android.os.Bundle; import android.widget.TextView; import android.widget.Toast; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; @@ -37,52 +38,38 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; -import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface; +import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil; -public class Watch9PairingActivity extends AbstractGBActivity { +import static nodomain.freeyourgadget.gadgetbridge.util.BondingUtil.STATE_DEVICE_CANDIDATE; + +public class Watch9PairingActivity extends AbstractGBActivity implements BondingInterface { private static final Logger LOG = LoggerFactory.getLogger(Watch9PairingActivity.class); - private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; - + private final BroadcastReceiver pairingReceiver = BondingUtil.getPairingReceiver(this); private TextView message; private GBDeviceCandidate deviceCandidate; - private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { - GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - LOG.debug("pairing activity: device changed: " + device); - if (deviceCandidate.getMacAddress().equals(device.getAddress())) { - if (device.isInitialized()) { - pairingFinished(); - } else if (device.isConnecting() || device.isInitializing()) { - LOG.info("still connecting/initializing device..."); - } - } - } - } - }; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_watch9_pairing); message = findViewById(R.id.watch9_pair_message); - Intent intent = getIntent(); - deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + + deviceCandidate = getIntent().getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); if (deviceCandidate == null && savedInstanceState != null) { deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); } + if (deviceCandidate == null) { Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show(); startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); finish(); return; } - startPairing(); + + startConnecting(deviceCandidate); } @Override @@ -97,33 +84,73 @@ public class Watch9PairingActivity extends AbstractGBActivity { deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); } + private void startConnecting(GBDeviceCandidate deviceCandidate) { + message.setText(getString(R.string.pairing, deviceCandidate)); + + removeBroadcastReceivers(); + BondingUtil.connectThenComplete(this, deviceCandidate); + } + + @Override + public void onBondingComplete(boolean success) { + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + finish(); + } + + @Override + public BluetoothDevice getCurrentTarget() { + return this.deviceCandidate.getDevice(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + BondingUtil.handleActivityResult(this, requestCode, resultCode, data); + } + + @Override + public void unregisterBroadcastReceivers() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), pairingReceiver); + } + + @Override + public void removeBroadcastReceivers() { + LocalBroadcastManager.getInstance(this).registerReceiver(pairingReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); + } + + @Override + public Context getContext() { + return this; + } + + @Override + protected void onResume() { + removeBroadcastReceivers(); + super.onResume(); + } + + @Override + protected void onStart() { + removeBroadcastReceivers(); + super.onStart(); + } + @Override protected void onDestroy() { - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + unregisterBroadcastReceivers(); super.onDestroy(); } - private void startPairing() { - message.setText(getString(R.string.pairing, deviceCandidate)); - - IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); - LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); - - GBApplication.deviceService().disconnect(); - GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); - if (device != null) { - GBApplication.deviceService().connect(device, true); - } else { - GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR); - } + @Override + protected void onStop() { + unregisterBroadcastReceivers(); + super.onStop(); } - private void pairingFinished() { - AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); - - Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - - finish(); + @Override + protected void onPause() { + // WARN: Do not remove listeners on pause + // Bonding process can pause the activity and you might miss broadcasts + super.onPause(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java index 395d7380c2..56104c5cfb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java @@ -30,13 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; -/** - * Created by jpbarraca on 13/04/2017. - */ - public class BluetoothPairingRequestReceiver extends BroadcastReceiver { - - private static final Logger LOG = LoggerFactory.getLogger(BluetoothConnectReceiver.class); final DeviceCommunicationService service; @@ -56,8 +50,9 @@ public class BluetoothPairingRequestReceiver extends BroadcastReceiver { GBDevice gbDevice = service.getGBDevice(); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (gbDevice == null || device == null) + if (gbDevice == null || device == null) { return; + } DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); try { @@ -67,7 +62,6 @@ public class BluetoothPairingRequestReceiver extends BroadcastReceiver { } } catch (Exception e) { LOG.warn("Could not abort pairing request process"); - } } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index cabc10d0ab..1ad55fc06d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -103,12 +103,12 @@ public class NotificationListener extends NotificationListenerService { public static final String ACTION_REPLY = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.reply"; - private LimitedQueue mActionLookup = new LimitedQueue(32); - private LimitedQueue mPackageLookup = new LimitedQueue(64); - private LimitedQueue mNotificationHandleLookup = new LimitedQueue(128); + private final LimitedQueue mActionLookup = new LimitedQueue(32); + private final LimitedQueue mPackageLookup = new LimitedQueue(64); + private final LimitedQueue mNotificationHandleLookup = new LimitedQueue(128); - private HashMap notificationBurstPrevention = new HashMap<>(); - private HashMap notificationOldRepeatPrevention = new HashMap<>(); + private final HashMap notificationBurstPrevention = new HashMap<>(); + private final HashMap notificationOldRepeatPrevention = new HashMap<>(); private static final Set GROUP_SUMMARY_WHITELIST = Collections.singleton( "mikado.bizcalpro" @@ -119,7 +119,7 @@ public class NotificationListener extends NotificationListenerService { private long activeCallPostTime; private int mLastCallCommand = CallSpec.CALL_UNDEFINED; - private Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(); private Runnable mSetMusicInfoRunnable = null; private Runnable mSetMusicStateRunnable = null; @@ -516,8 +516,7 @@ public class NotificationListener extends NotificationListenerService { GBApplication.deviceService().onSetCallState(callSpec); } - boolean shouldContinueAfterFilter(@NonNull String body, @NonNull List wordsList, @NonNull NotificationFilter notificationFilter) { - + boolean shouldContinueAfterFilter(String body, @NonNull List wordsList, @NonNull NotificationFilter notificationFilter) { LOG.debug("Mode: '{}' Submode: '{}' WordsList: '{}'", notificationFilter.getNotificationFilterMode(), notificationFilter.getNotificationFilterSubMode(), wordsList); boolean allMode = notificationFilter.getNotificationFilterSubMode() == NOTIFICATION_FILTER_SUBMODE_ALL; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index f46a67dd4f..6a759e5011 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.model; import androidx.annotation.Nullable; + import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; @@ -139,7 +140,7 @@ public interface DeviceService extends EventHandler { void connect(@Nullable GBDevice device); - void connect(@Nullable GBDevice device, boolean performPair); + void connect(@Nullable GBDevice device, boolean firstTime); void disconnect(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index c3701fb5a7..5d7db5401c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -29,6 +29,10 @@ import android.net.Uri; import android.os.Build; import android.telephony.SmsManager; +import androidx.core.app.NotificationCompat; +import androidx.core.content.FileProvider; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,14 +44,10 @@ import java.util.Date; import java.util.Locale; import java.util.Objects; -import androidx.core.app.NotificationCompat; -import androidx.core.content.FileProvider; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.FindPhoneActivity; import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment; -import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; @@ -186,12 +186,11 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { } private void handleGBDeviceEventFindPhoneStart() { - if ( Build.VERSION.SDK_INT < 29 ) { // this could be used if app in foreground + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // this could be used if app in foreground // TODO: Below Q? Intent startIntent = new Intent(getContext(), FindPhoneActivity.class); startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(startIntent); - } - else { + } else { handleGBDeviceEventFindPhoneStartNotification(); } } @@ -231,7 +230,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { LOG.info("Got event for CALL_CONTROL"); if(callEvent.event == GBDeviceEventCallControl.Event.IGNORE) { LOG.info("Sending intent for mute"); - Intent broadcastIntent = new Intent("nodomain.freeyourgadget.gadgetbridge.MUTE_CALL"); + Intent broadcastIntent = new Intent(context.getPackageName() + ".MUTE_CALL"); broadcastIntent.setPackage(context.getPackageName()); context.sendBroadcast(broadcastIntent); return; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 65b6c35881..e75265de58 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -146,6 +146,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_BROADCAST; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_OFF; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST; @@ -157,8 +159,6 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VI import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL; -import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_BROADCAST; -import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_OFF; public class HuamiSupport extends AbstractBTLEDeviceSupport { @@ -335,7 +335,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { @Override public boolean connectFirstTime() { needsAuth = true; - return super.connect(); + return connect(); } private HuamiSupport sendDefaultNotification(TransactionBuilder builder, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index f52a3292a7..e71317b883 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -202,6 +202,7 @@ class PebbleIoThread extends GBDeviceIoThread { final UUID UuidSDP = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); mBtSocket = btDevice.createRfcommSocketToServiceRecord(UuidSDP); + // TODO: Why is this comment here? //mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); mBtSocket.connect(); mInStream = mBtSocket.getInputStream(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index 32a31dc602..eb9e19ca50 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -73,7 +73,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { public void onInstallApp(Uri uri) { PebbleProtocol pebbleProtocol = (PebbleProtocol) getDeviceProtocol(); PebbleIoThread pebbleIoThread = getDeviceIOThread(); - // catch fake urls first + // Catch fake URLs first if (uri.equals(Uri.parse("fake://health"))) { getDeviceIOThread().write(pebbleProtocol.encodeActivateHealth(true)); String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, getContext().getString(R.string.p_unit_metric)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java index afcfaeac3c..73271fac04 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java @@ -59,8 +59,8 @@ class PebbleGATTClient extends BluetoothGattCallback { private final PebbleLESupport mPebbleLESupport; private boolean oldPebble = false; - private boolean doPairing = true; - private boolean removeBond = false; + private final boolean doPairing = true; + private final boolean removeBond = false; private BluetoothGatt mBluetoothGatt; private CountDownLatch mWaitWriteCompleteLatch; @@ -83,7 +83,7 @@ class PebbleGATTClient extends BluetoothGattCallback { } else if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_READ)) { mPebbleLESupport.handlePPoGATTPacket(characteristic.getValue().clone()); } else { - LOG.info("onCharacteristicChanged()" + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); + LOG.info("onCharacteristicChanged() " + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); } } @@ -94,7 +94,7 @@ class PebbleGATTClient extends BluetoothGattCallback { LOG.info("onCharacteristicRead() status = " + status); if (status == BluetoothGatt.GATT_SUCCESS) { - LOG.info("onCharacteristicRead()" + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); + LOG.info("onCharacteristicRead() " + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); if (oldPebble) { subscribeToConnectivity(gatt); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingInterface.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingInterface.java new file mode 100644 index 0000000000..f416fe2853 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingInterface.java @@ -0,0 +1,50 @@ +/* Copyright (C) 2020 Taavi Eomäe + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.util; + + +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +public interface BondingInterface { + /** + * Called when pairing is complete + **/ + void onBondingComplete(boolean success); + + /** + * Should return the device that is currently being paired + **/ + BluetoothDevice getCurrentTarget(); + + /** + * This forces bonding activities to encapsulate the removal + * of all broadcast receivers on demand + **/ + void unregisterBroadcastReceivers(); + + /** + * This forces bonding activities to handle the addition + * of all broadcast receivers in the same place + **/ + void removeBroadcastReceivers(); + + /** + * Just returns the Context + */ + Context getContext(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java new file mode 100644 index 0000000000..8622aa6a0e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java @@ -0,0 +1,386 @@ +/* Copyright (C) 2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, Taavi Eomäe + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.app.Activity; +import android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.companion.AssociationRequest; +import android.companion.BluetoothDeviceFilter; +import android.companion.CompanionDeviceManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.annotation.RequiresApi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; + +import static androidx.core.app.ActivityCompat.startIntentSenderForResult; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast; + +public class BondingUtil { + public static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; + + private static final int REQUEST_CODE = 1; + private static final Logger LOG = LoggerFactory.getLogger(BondingUtil.class); + private static final long DELAY_AFTER_BONDING = 1000; // 1s + + /** + * Returns a BroadcastReceiver that handles Gadgetbridge's device changed broadcasts + */ + public static BroadcastReceiver getPairingReceiver(final BondingInterface activity) { + return new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { + GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + LOG.debug("Pairing receiver: device changed: " + device); + if (activity.getCurrentTarget().getAddress().equals(device.getAddress())) { + if (device.isInitialized()) { + LOG.info("Device is initialized, finish things up"); + activity.onBondingComplete(true); + } else if (device.isConnecting() || device.isInitializing()) { + LOG.info("Still connecting/initializing device..."); + } + } + } + } + }; + } + + /** + * Returns a BroadcastReceiver that handles Bluetooth chance broadcasts + */ + public static BroadcastReceiver getBondingReceiver(final BondingInterface bondingInterface) { + return new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + String bondingMacAddress = bondingInterface.getCurrentTarget().getAddress(); + + LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + bondingMacAddress); + if (bondingMacAddress != null && bondingMacAddress.equals(device.getAddress())) { + int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); + switch (bondState) { + case BluetoothDevice.BOND_BONDED: { + LOG.info("Bonded with " + device.getAddress()); + //noinspection StatementWithEmptyBody + if (isLePebble(device)) { + // Do not initiate connection to LE Pebble! + } else { + attemptToFirstConnect(bondingInterface.getCurrentTarget()); + } + return; + } + case BluetoothDevice.BOND_NONE: { + LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway."); + attemptToFirstConnect(bondingInterface.getCurrentTarget()); + return; + } + case BluetoothDevice.BOND_BONDING: { + LOG.info("Bonding in progress with " + device.getAddress()); + return; + } + default: { + LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState); + bondingInterface.onBondingComplete(false); + } + } + } + } + } + }; + } + + /** + * Connect to candidate after a certain delay + * + * @param candidate the device to connect to + */ + public static void attemptToFirstConnect(final BluetoothDevice candidate) { + Looper mainLooper = Looper.getMainLooper(); + new Handler(mainLooper).postDelayed(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().disconnect(); + GBDevice device = DeviceHelper.getInstance().toSupportedDevice(candidate); + connectToGBDevice(device); + } + }, DELAY_AFTER_BONDING); + } + + /** + * Just calls DeviceService connect with the "first time" flag + */ + private static void connectToGBDevice(GBDevice device) { + if (device != null) { + GBApplication.deviceService().connect(device, true); + } else { + GB.toast("Unable to connect, can't recognize the device type", Toast.LENGTH_LONG, GB.ERROR); + } + } + + + /** + * Returns true if GB should pair + */ + public static boolean shouldUseBonding() { + // TODO: Migrate to generic "should even try bonding" preference key + + // There are connection problems on certain Galaxy S devices at least + // try to connect without BT pairing (bonding) + Prefs prefs = GBApplication.getPrefs(); + return prefs.getPreferences().getBoolean(MiBandConst.PREF_MIBAND_SETUP_BT_PAIRING, true); + } + + /** + * Connects to the device and calls callback + */ + public static void connectThenComplete(BondingInterface bondingInterface, GBDeviceCandidate deviceCandidate) { + GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); + connectThenComplete(bondingInterface, device); + } + + /** + * Connects to the device and calls callback + */ + public static void connectThenComplete(BondingInterface bondingInterface, GBDevice device) { + toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.discovery_trying_to_connect_to, device.getName()), Toast.LENGTH_SHORT, GB.INFO); + // Disconnect when LE Pebble so that the user can manually initiate a connection + GBApplication.deviceService().disconnect(); + GBApplication.deviceService().connect(device); + bondingInterface.onBondingComplete(true); + } + + /** + * Checks the type of bonding needed for the device and continues accordingly + */ + public static void initiateCorrectBonding(final BondingInterface bondingInterface, final GBDeviceCandidate deviceCandidate) { + int bondingStyle = DeviceHelper.getInstance().getCoordinator(deviceCandidate).getBondingStyle(); + if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) { + // Do nothing + return; + } else if (bondingStyle == DeviceCoordinator.BONDING_STYLE_ASK) { + new AlertDialog.Builder(bondingInterface.getContext()) + .setCancelable(true) + .setTitle(bondingInterface.getContext().getString(R.string.discovery_pair_title, deviceCandidate.getName())) + .setMessage(bondingInterface.getContext().getString(R.string.discovery_pair_question)) + .setPositiveButton(bondingInterface.getContext().getString(R.string.discovery_yes_pair), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + BondingUtil.tryBondThenComplete(bondingInterface, deviceCandidate); + } + }) + .setNegativeButton(R.string.discovery_dont_pair, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + BondingUtil.connectThenComplete(bondingInterface, deviceCandidate); + } + }) + .show(); + } else { + BondingUtil.tryBondThenComplete(bondingInterface, deviceCandidate); + } + LOG.debug("Bonding initiated"); + } + + /** + * Tries to create a BluetoothDevice bond + * Do not call directly, use createBond(Activity, GBDeviceCandidate) instead! + */ + private static void bluetoothBond(BondingInterface context, BluetoothDevice device) { + if (device.createBond()) { + // Async, results will be delivered via a broadcast + LOG.info("Bonding in progress..."); + } else { + toast(context.getContext(), context.getContext().getString(R.string.discovery_bonding_failed_immediately, device.getName()), Toast.LENGTH_SHORT, GB.ERROR); + } + } + + /** + * Handles the activity result and checks if there's anything CompanionDeviceManager-related going on + */ + public static void handleActivityResult(BondingInterface bondingInterface, int requestCode, int resultCode, Intent data) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + requestCode == BondingUtil.REQUEST_CODE && + resultCode == Activity.RESULT_OK) { + + BluetoothDevice deviceToPair = + data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE); + + if (deviceToPair != null) { + if (bondingInterface.getCurrentTarget().getAddress().equals(deviceToPair.getAddress())) { + if (deviceToPair.getBondState() != BluetoothDevice.BOND_BONDED) { + BondingUtil.bluetoothBond(bondingInterface, bondingInterface.getCurrentTarget()); + } else { + bondingInterface.onBondingComplete(true); + } + } else { + bondingInterface.onBondingComplete(false); + } + } + } + } + + /** + * Checks if device is LE Pebble + */ + public static boolean isLePebble(BluetoothDevice device) { + return (device.getType() == BluetoothDevice.DEVICE_TYPE_DUAL || device.getType() == BluetoothDevice.DEVICE_TYPE_LE) && + (device.getName().startsWith("Pebble-LE ") || device.getName().startsWith("Pebble Time LE ")); + } + + /** + * Uses the CompanionDeviceManager bonding method + */ + @RequiresApi(Build.VERSION_CODES.O) + private static void companionDeviceManagerBond(BondingInterface bondingInterface, + final GBDeviceCandidate deviceCandidate) { + BluetoothDeviceFilter deviceFilter = new BluetoothDeviceFilter.Builder() + .setAddress(deviceCandidate.getMacAddress()) + .build(); + + AssociationRequest pairingRequest = new AssociationRequest.Builder() + .addDeviceFilter(deviceFilter) + .setSingleDevice(true) + .build(); + + CompanionDeviceManager manager = (CompanionDeviceManager) bondingInterface.getContext().getSystemService(Context.COMPANION_DEVICE_SERVICE); + for (String association : manager.getAssociations()) { + if (association.equals(deviceCandidate.getMacAddress())) { + LOG.info("The device has already been bonded through CompanionDeviceManager, using regular"); + // If it's already "associated", we should immediately pair + // because the callback is never called (AFAIK?) + BondingUtil.bluetoothBond(bondingInterface, deviceCandidate.getDevice()); + return; + } + } + + manager.associate(pairingRequest, + getCompanionDeviceManagerCallback(bondingInterface), + null); + } + + /** + * This is a bit hacky, but it does stop a bonding that might be otherwise stuck, + * use with some caution + */ + public static void stopBluetoothBonding(BluetoothDevice device) { + try { + //noinspection JavaReflectionMemberAccess + device.getClass().getMethod("cancelBondProcess").invoke(device); + } catch (Throwable ignore) { + } + } + + /** + * Finalizes bonded device + */ + public static void handleDeviceBonded(BondingInterface bondingInterface, GBDeviceCandidate deviceCandidate) { + if (deviceCandidate == null) { + LOG.error("deviceCandidate was null! Can't handle bonded device!"); + return; + } + + toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.discovery_successfully_bonded, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO); + connectThenComplete(bondingInterface, deviceCandidate); + } + + /** + * Use this function to initiate bonding to a GBDeviceCandidate + */ + public static void tryBondThenComplete(BondingInterface bondingInterface, GBDeviceCandidate deviceCandidate) { + bondingInterface.removeBroadcastReceivers(); + BluetoothDevice device = deviceCandidate.getDevice(); + + int bondState = device.getBondState(); + if (bondState == BluetoothDevice.BOND_BONDED) { + GB.toast(bondingInterface.getContext().getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO); + //noinspection StatementWithEmptyBody + if (GBApplication.getPrefs().getBoolean("enable_companiondevice_pairing", true) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // If CompanionDeviceManager is enabled, skip connection and go bond + // TODO: It would theoretically be nice to check if it's already been granted, + // but re-bond works + } else { + attemptToFirstConnect(bondingInterface.getCurrentTarget()); + return; + } + } else if (bondState == BluetoothDevice.BOND_BONDING) { + GB.toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.pairing_in_progress, device.getName(), device.getAddress()), Toast.LENGTH_LONG, GB.INFO); + return; + } + + GB.toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.pairing_creating_bond_with, device.getName(), device.getAddress()), Toast.LENGTH_LONG, GB.INFO); + toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.discovery_attempting_to_pair, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO); + if (GBApplication.getPrefs().getBoolean("enable_companiondevice_pairing", true) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + companionDeviceManagerBond(bondingInterface, deviceCandidate); + } else { + bluetoothBond(bondingInterface, deviceCandidate.getDevice()); + } + } + + /** + * Returns a callback for CompanionDeviceManager + * + * @param bondingInterface the activity that started the CDM bonding process + * @return CompanionDeviceManager.Callback that handles the CompanionDeviceManager bonding process results + */ + @RequiresApi(Build.VERSION_CODES.O) + private static CompanionDeviceManager.Callback getCompanionDeviceManagerCallback(final BondingInterface bondingInterface) { + return new CompanionDeviceManager.Callback() { + @Override + public void onFailure(CharSequence error) { + toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.discovery_bonding_failed_immediately), Toast.LENGTH_SHORT, GB.ERROR); + } + + @Override + public void onDeviceFound(IntentSender chooserLauncher) { + try { + startIntentSenderForResult((Activity) bondingInterface.getContext(), + chooserLauncher, + REQUEST_CODE, + null, + 0, + 0, + 0, + null); + } catch (IntentSender.SendIntentException e) { + LOG.error(e.toString()); + } + } + }; + } +}