From 07d661f7cb46a02116a6df95acfc79f1b5185498 Mon Sep 17 00:00:00 2001
From: Aleksander Nowakowski
+ * If the new firmware does not share the bond information with the old one the bond information is lost. Set this flag to
+ * In case of updating the soft device and bootloader the application is always removed so the bond information with it.
+ *
+ * Search for occurrences of EXTRA_RESTORE_BOND in this file to check the implementation and get more details.
+ * true
to make the service create new bond with the new
+ * application when the upload is done (and remove the old one). When set to false
(default), the DFU service assumes that the LTK is shared between them. Currently it is not possible
+ * to remove the old bond and not creating a new one so if your old application supported bonding while the new one does not you have to modify the source code yourself.
+ *
*
*
*
+ *
+ */
+ public static final int PROGRESS_ENABLING_DFU_MODE = -3;
/** Service is sending validation request to the remote DFU target. */
public static final int PROGRESS_VALIDATING = -4;
/** Service is disconnecting from the DFU target. */
@@ -459,6 +487,29 @@ public abstract class DfuBaseService extends IntentService {
}
};
+ private final BroadcastReceiver mBondStateBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ // obtain the device and check it this is the one that we are connected to
+ final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (!device.getAddress().equals(mDeviceAddress))
+ return;
+
+ // read bond state
+ final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+ if (bondState == BluetoothDevice.BOND_BONDING)
+ return;
+
+ mRequestCompleted = true;
+
+ // notify waiting thread
+ synchronized (mLock) {
+ mLock.notifyAll();
+ return;
+ }
+ };
+ };
+
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
@@ -747,9 +798,11 @@ public abstract class DfuBaseService extends IntentService {
// We must register this as a non-local receiver to get broadcasts from the notification action
registerReceiver(mDfuActionReceiver, actionFilter);
- final IntentFilter filter = new IntentFilter();
- filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+ final IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);
registerReceiver(mConnectionStateBroadcastReceiver, filter);
+
+ final IntentFilter bondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ registerReceiver(mBondStateBroadcastReceiver, bondFilter);
}
@Override
@@ -761,6 +814,7 @@ public abstract class DfuBaseService extends IntentService {
unregisterReceiver(mDfuActionReceiver);
unregisterReceiver(mConnectionStateBroadcastReceiver);
+ unregisterReceiver(mBondStateBroadcastReceiver);
}
@Override
@@ -816,7 +870,7 @@ public abstract class DfuBaseService extends IntentService {
mRequestCompleted = false;
mImageSizeSent = false;
- // read preferences
+ // Read preferences
final boolean packetReceiptNotificationEnabled = preferences.getBoolean(DfuSettingsConstants.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, true);
String value = preferences.getString(DfuSettingsConstants.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(DfuSettingsConstants.SETTINGS_NUMBER_OF_PACKETS_DEFAULT));
int numberOfPackets = DfuSettingsConstants.SETTINGS_NUMBER_OF_PACKETS_DEFAULT;
@@ -844,6 +898,9 @@ public abstract class DfuBaseService extends IntentService {
sendLogBroadcast(Level.VERBOSE, "Starting DFU service");
+ /*
+ * First the service is trying to read the firmware and init packet files.
+ */
InputStream is = null;
InputStream initIs = null;
int imageSizeInBytes;
@@ -897,7 +954,10 @@ public abstract class DfuBaseService extends IntentService {
return;
}
- // Let's connect to the device
+ /*
+ * Now let's connect to the device.
+ * All the methods below are synchronous. The mLock object is used to wait for asynchronous calls.
+ */
sendLogBroadcast(Level.VERBOSE, "Connecting to DFU target...");
updateProgressNotification(PROGRESS_CONNECTING);
@@ -955,7 +1015,7 @@ public abstract class DfuBaseService extends IntentService {
try {
updateProgressNotification(PROGRESS_STARTING);
- // read the version number if available
+ // Read the version number if available. The version number consists of 2 bytes: major and minor. Therefore f.e. the version 5 (00-05) can be read as 0.5.
int version = 0;
if (versionCharacteristic != null) {
version = readVersion(gatt, versionCharacteristic);
@@ -970,7 +1030,7 @@ public abstract class DfuBaseService extends IntentService {
* In the DFU from SDK 6.1, which was also supporting the buttonless update, there was no DFU Version characteristic. In that case we may find out whether
* we are in the bootloader or application by simply checking the number of characteristics.
*/
- if (version == 1 || dfuService.getCharacteristics().size() > 3 /* Generic Access, Generic Attribute, DFU Service */) {
+ if (version == 1 || dfuService.getCharacteristics().size() == 3 /* Generic Access, Generic Attribute, DFU Service */) {
// the service is connected to the application, not to the bootloader
logi("Application with buttonless update found");
sendLogBroadcast(Level.WARNING, "Application with buttonless update found");
@@ -989,12 +1049,12 @@ public abstract class DfuBaseService extends IntentService {
sendLogBroadcast(Level.VERBOSE, "Jumping to the DFU Bootloader...");
- // enable notifications
+ // Enable notifications
enableCCCD(gatt, controlPointCharacteristic, NOTIFICATIONS);
sendLogBroadcast(Level.APPLICATION, "Notifications enabled");
- // send 'jump to bootloader command' (Start DFU)
- updateProgressNotification(PROGRESS_DISCONNECTING);
+ // Send 'jump to bootloader command' (Start DFU)
+ updateProgressNotification(PROGRESS_ENABLING_DFU_MODE);
OP_CODE_START_DFU[1] = 0x04;
logi("Sending Start DFU command (Op Code = 1, Upload Mode = 4)");
writeOpCode(gatt, controlPointCharacteristic, OP_CODE_START_DFU, true);
@@ -1004,23 +1064,48 @@ public abstract class DfuBaseService extends IntentService {
waitUntilDisconnected();
sendLogBroadcast(Level.INFO, "Disconnected by the remote device");
+ /*
+ * We would like to avoid using the hack with refreshing the device (refresh method is not in the public API). The refresh method clears the cached services and causes a
+ * service discovery afterwards (when connected). Android, however, does it itself when receive the Service Changed indication when bonded.
+ * In case of unpaired device we may either refresh the services manually (using the hack), or use a different device address in bootloader more.
+ * Therefore, since the version 0.6 of the DFU bootloader (available in SDK 8.0), refreshing is not required as this version uses both those methods.
+ * If you need to use the experimental DFU bootloader in version 0.5 (SDK 7.1) you have to uncomment the following line.
+ * Unfortunately while being in the application mode we don't know the bootloader version.
+ */
+ // refreshDeviceCache(gatt, true);
+
+ String bootloaderAddress = deviceAddress;
+ if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
+ logi("Scanning for the bootloader (address = " + deviceAddress + ")");
+ sendLogBroadcast(Level.VERBOSE, "Scanning for the bootloader...");
+
+ final BootloaderScanner scanner = BootloaderScannerFactory.getScanner();
+ bootloaderAddress = scanner.searchFor(deviceAddress);
+ logi("Bootloader device found: " + bootloaderAddress);
+ sendLogBroadcast(Level.APPLICATION, "Bootloader with address " + bootloaderAddress + " found");
+
+ // In case the device is not bonded and it is advertising with the same address as before we must refresh the services manually.
+ if (bootloaderAddress.equals(deviceAddress))
+ refreshDeviceCache(gatt, false /* or true, doesn't matter as not bonded */);
+ }
+
// Close the device
- refreshDeviceCache(gatt, false);
close(gatt);
logi("Starting service that will connect to the DFU bootloader");
final Intent newIntent = new Intent();
newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE);
+ newIntent.putExtra(EXTRA_DEVICE_ADDRESS, bootloaderAddress);
startService(newIntent);
return;
}
- // enable notifications
+ // Enable notifications
enableCCCD(gatt, controlPointCharacteristic, NOTIFICATIONS);
sendLogBroadcast(Level.APPLICATION, "Notifications enabled");
try {
- // set up the temporary variable that will hold the responses
+ // Set up the temporary variable that will hold the responses
byte[] response = null;
int status = 0;
@@ -1053,11 +1138,11 @@ public abstract class DfuBaseService extends IntentService {
* 2. In case of SD or BL update an error is returned
*/
- // obtain size of image(s)
+ // Obtain size of image(s)
int softDeviceImageSize = (fileType & TYPE_SOFT_DEVICE) > 0 ? imageSizeInBytes : 0;
int bootloaderImageSize = (fileType & TYPE_BOOTLOADER) > 0 ? imageSizeInBytes : 0;
int appImageSize = (fileType & TYPE_APPLICATION) > 0 ? imageSizeInBytes : 0;
- // the sizes above may be overwritten if a ZIP file was passed
+ // The sizes above may be overwritten if a ZIP file was passed
if (MIME_TYPE_ZIP.equals(mimeType)) {
final ZipHexInputStream zhis = (ZipHexInputStream) is;
softDeviceImageSize = zhis.softDeviceImageSize();
@@ -1222,6 +1307,7 @@ public abstract class DfuBaseService extends IntentService {
}
logi("Sending the Initialize DFU Parameters COMPLETE (Op Code = 2, Value = 1)");
writeOpCode(gatt, controlPointCharacteristic, OP_CODE_INIT_DFU_PARAMS_COMPLETE);
+ sendLogBroadcast(Level.APPLICATION, "Initialize DFU Parameters completed");
// a notification will come with confirmation. Let's wait for it a bit
response = readNotificationResponse();
@@ -1229,7 +1315,6 @@ public abstract class DfuBaseService extends IntentService {
sendLogBroadcast(Level.APPLICATION, "Responce received (Op Code = " + response[1] + ", Status = " + status + ")");
if (status != DFU_STATUS_SUCCESS)
throw new RemoteDfuException("Device returned error after sending init packet", status);
- sendLogBroadcast(Level.APPLICATION, "Initialize DFU Parameters completed");
} else
mInitPacketSent = true;
@@ -1296,10 +1381,41 @@ public abstract class DfuBaseService extends IntentService {
waitUntilDisconnected();
sendLogBroadcast(Level.INFO, "Disconnected by the remote device");
+ /*
+ * Since version 0.6 the unpaired bootloader advertises as a different device (with the last byte of its address incremented by 1).
+ * Therefore there is no need to refresh the cache of the current (DFU bootloader) device.
+ */
+ if (version < 6)
+ refreshDeviceCache(gatt, true); // The new application may have lost bonding information (if there was bonding). Force refresh it just for sure.
// Close the device
- refreshDeviceCache(gatt, true); // The new application may have lost bonding information (if there was bonding). Force refresh it just for sure.
close(gatt);
+ // During the update the bonding information on the target device may have been removed.
+ // To create bond with the new application set the EXTRA_RESTORE_BOND extra to true.
+ // In case the bond information is copied to the new application the new bonding is not required.
+ if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
+ final boolean restoreBond = intent.getBooleanExtra(EXTRA_RESTORE_BOND, false);
+
+ if (restoreBond || (fileType & (TYPE_SOFT_DEVICE | TYPE_BOOTLOADER)) > 0) {
+ // In case the SoftDevice and Bootloader were updated the bond information was lost.
+ removeBond(gatt.getDevice());
+
+ // Give some time for removing the bond information. 300ms was to short, let's set it to 2 seconds just to be sure.
+ synchronized (this) {
+ try {
+ wait(2000);
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+
+ if (restoreBond && (fileType & TYPE_APPLICATION) > 0) {
+ // Restore pairing when application was updated.
+ createBond(gatt.getDevice());
+ }
+ }
+
/*
* We need to send PROGRESS_COMPLETED message only when all files has been transmitted.
* In case user wants to send Soft Device and/or Bootloader and Application service will be started twice: one to send SD+BL,
@@ -2003,6 +2119,90 @@ public abstract class DfuBaseService extends IntentService {
}
}
+ @SuppressLint("NewApi")
+ public boolean createBond(final BluetoothDevice device) {
+ if (device.getBondState() == BluetoothDevice.BOND_BONDED)
+ return true;
+
+ boolean result = false;
+ mRequestCompleted = false;
+
+ sendLogBroadcast(Level.VERBOSE, "Starting pairing...");
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ sendLogBroadcast(Level.DEBUG, "gatt.getDevice().createBond()");
+ result = device.createBond();
+ } else {
+ result = createBondApi18(device);
+ }
+
+ // We have to wait until device is bounded
+ try {
+ synchronized (mLock) {
+ while (mRequestCompleted == false && !mAborted)
+ mLock.wait();
+ }
+ } catch (final InterruptedException e) {
+ loge("Sleeping interrupted", e);
+ }
+ return result;
+ }
+
+ public boolean createBondApi18(final BluetoothDevice device) {
+ /*
+ * There is a createBond() method in BluetoothDevice class but for now it's hidden. We will call it using reflections. It has been revealed in KitKat (Api19)
+ */
+ try {
+ final Method createBond = device.getClass().getMethod("createBond");
+ if (createBond != null) {
+ sendLogBroadcast(Level.DEBUG, "gatt.getDevice().createBond() (hidden)");
+ return (Boolean) createBond.invoke(device);
+ }
+ } catch (final Exception e) {
+ Log.w(TAG, "An exception occurred while creating bond", e);
+ }
+ return false;
+ }
+
+ /**
+ * Removes the bond information for the given device.
+ *
+ * @param device
+ * the device to unbound
+ * @return true
if operation succeeded, false
otherwise
+ */
+ public boolean removeBond(final BluetoothDevice device) {
+ if (device.getBondState() == BluetoothDevice.BOND_NONE)
+ return true;
+
+ sendLogBroadcast(Level.VERBOSE, "Removing bond information...");
+ boolean result = false;
+ /*
+ * There is a removeBond() method in BluetoothDevice class but for now it's hidden. We will call it using reflections.
+ */
+ try {
+ final Method removeBond = device.getClass().getMethod("removeBond");
+ if (removeBond != null) {
+ mRequestCompleted = false;
+ sendLogBroadcast(Level.DEBUG, "gatt.getDevice().removeBond() (hidden)");
+ result = (Boolean) removeBond.invoke(device);
+
+ // We have to wait until device is unbounded
+ try {
+ synchronized (mLock) {
+ while (mRequestCompleted == false && !mAborted)
+ mLock.wait();
+ }
+ } catch (final InterruptedException e) {
+ loge("Sleeping interrupted", e);
+ }
+ }
+ result = true;
+ } catch (final Exception e) {
+ Log.w(TAG, "An exception occurred while removing bond information", e);
+ }
+ return result;
+ }
+
/**
* Waits until the notification will arrive. Returns the data returned by the notification. This method will block the thread if response is not ready or connection state will change from
* {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown.
@@ -2050,14 +2250,14 @@ public abstract class DfuBaseService extends IntentService {
* Creates or updates the notification in the Notification Manager. Sends broadcast with given progress or error state to the activity.
*
* @param progress
- * the current progress state or an error number, can be one of {@link #PROGRESS_CONNECTING}, {@link #PROGRESS_STARTING}, {@link #PROGRESS_VALIDATING}, {@link #PROGRESS_DISCONNECTING},
- * {@link #PROGRESS_COMPLETED} or {@link #ERROR_FILE_ERROR}, {@link #ERROR_FILE_INVALID} , etc
+ * the current progress state or an error number, can be one of {@link #PROGRESS_CONNECTING}, {@link #PROGRESS_STARTING}, {@link #PROGRESS_ENABLING_DFU_MODE},
+ * {@link #PROGRESS_VALIDATING}, {@link #PROGRESS_DISCONNECTING}, {@link #PROGRESS_COMPLETED} or {@link #ERROR_FILE_ERROR}, {@link #ERROR_FILE_INVALID} , etc
*/
private void updateProgressNotification(final int progress) {
final String deviceAddress = mDeviceAddress;
final String deviceName = mDeviceName != null ? mDeviceName : getString(R.string.dfu_unknown_name);
- final Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_notify_dfu);
+ // final Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_notify_dfu); <- this looks bad on Android 5
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setSmallIcon(android.R.drawable.stat_sys_upload).setOnlyAlertOnce(true);//.setLargeIcon(largeIcon);
switch (progress) {
@@ -2067,6 +2267,10 @@ public abstract class DfuBaseService extends IntentService {
case PROGRESS_STARTING:
builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_starting)).setContentText(getString(R.string.dfu_status_starting_msg, deviceName)).setProgress(100, 0, true);
break;
+ case PROGRESS_ENABLING_DFU_MODE:
+ builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_switching_to_dfu)).setContentText(getString(R.string.dfu_status_switching_to_dfu_msg, deviceName))
+ .setProgress(100, 0, true);
+ break;
case PROGRESS_VALIDATING:
builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_validating)).setContentText(getString(R.string.dfu_status_validating_msg, deviceName)).setProgress(100, 0, true);
break;
@@ -2075,15 +2279,18 @@ public abstract class DfuBaseService extends IntentService {
.setProgress(100, 0, true);
break;
case PROGRESS_COMPLETED:
- builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_completed)).setContentText(getString(R.string.dfu_status_completed_msg)).setAutoCancel(true);
+ builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_completed)).setSmallIcon(android.R.drawable.stat_sys_upload_done)
+ .setContentText(getString(R.string.dfu_status_completed_msg)).setAutoCancel(true);
break;
case PROGRESS_ABORTED:
- builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_aborted)).setContentText(getString(R.string.dfu_status_aborted_msg)).setAutoCancel(true);
+ builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_aborted)).setSmallIcon(android.R.drawable.stat_sys_upload_done)
+ .setContentText(getString(R.string.dfu_status_aborted_msg)).setAutoCancel(true);
break;
default:
if (progress >= ERROR_MASK) {
// progress is an error number
- builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_error)).setContentText(getString(R.string.dfu_status_error_msg)).setAutoCancel(true);
+ builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_error)).setSmallIcon(android.R.drawable.stat_sys_upload_done)
+ .setContentText(getString(R.string.dfu_status_error_msg)).setAutoCancel(true);
} else {
// progress is in percents
final String title = mPartsTotal == 1 ? getString(R.string.dfu_status_uploading) : getString(R.string.dfu_status_uploading_part, mPartCurrent, mPartsTotal);
@@ -2143,6 +2350,7 @@ public abstract class DfuBaseService extends IntentService {
*
+ * The scanner is being used only with unpaired devices. When bonded, the device will always keep the same address and preserve the Long Term Key while in the bootloader mode. It will notify the + * application about the DFU services using the Service Changed indication. + *
+ */ +public interface BootloaderScanner { + /** + * After the buttonless jump from the application mode to the bootloader mode the service will wait this long for the advertising bootloader (in milliseconds). + */ + public final static long TIMEOUT = 2000l; // ms + /** The bootloader may advertise with the same address or one with the last byte incremented by this value. F.e. 00:11:22:33:44:55 -> 00:11:22:33:44:56. FF changes to 00. */ + public final static int ADDRESS_DIFF = 1; + + /** + * Searches for the advertising bootloader. The bootloader may advertise with the same device address or one with the last byte incremented by 1. + * This method is a blocking one and ends when such device is found. There are two implementations of this interface - one for old Androids and one for + * the Android 5+ devices. + * + * @param deviceAddress + * the application device address + * @return the address of the advertising DFU bootloader. If may be the same as the application address or one with the last byte incremented by 1 (AA:BB:CC:DD:EE:45/FF -> AA:BB:CC:DD:EE:46/00). + */ + public String searchFor(final String deviceAddress); +} diff --git a/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerFactory.java b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerFactory.java new file mode 100644 index 0000000..95728de --- /dev/null +++ b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerFactory.java @@ -0,0 +1,17 @@ +package no.nordicsemi.android.dfu.scanner; + +import android.os.Build; + +public class BootloaderScannerFactory { + + /** + * Returns the scanner implementation. + * + * @return the bootloader scanner + */ + public static BootloaderScanner getScanner() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return new BootloaderScannerLollipop(); + return new BootloaderScannerJB(); + } +} diff --git a/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerJB.java b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerJB.java new file mode 100644 index 0000000..035278c --- /dev/null +++ b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerJB.java @@ -0,0 +1,76 @@ +package no.nordicsemi.android.dfu.scanner; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.os.Handler; + +/** + * @see BootloaderScanner + */ +public class BootloaderScannerJB implements BootloaderScanner, BluetoothAdapter.LeScanCallback { + private Object mLock = new Object(); + private String mDeviceAddress; + private String mDeviceAddressIncremented; + private String mBootloaderAddress; + private boolean mFound; + + @SuppressWarnings("deprecation") + @Override + public String searchFor(final String deviceAddress) { + final String fistBytes = deviceAddress.substring(0, 15); + final String lastByte = deviceAddress.substring(15); // assuming that the device address is correct + final String lastByteIncremented = String.format("%02X", (Integer.valueOf(lastByte, 16) + ADDRESS_DIFF) & 0xFF); + + mDeviceAddress = deviceAddress; + mDeviceAddressIncremented = fistBytes + lastByteIncremented; + mBootloaderAddress = null; + mFound = false; + + // Add timeout + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (mFound) + return; + + mBootloaderAddress = null; + mFound = true; + + // Notify the waiting thread + synchronized (mLock) { + mLock.notifyAll(); + } + } + }, BootloaderScanner.TIMEOUT); + + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + adapter.startLeScan(this); + + try { + synchronized (mLock) { + while (!mFound) + mLock.wait(); + } + } catch (final InterruptedException e) { + // do nothing + } + + adapter.stopLeScan(this); + return mBootloaderAddress; + } + + @Override + public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) { + final String address = device.getAddress(); + if (mDeviceAddress.equals(address) || mDeviceAddressIncremented.equals(address)) { + mBootloaderAddress = address; + mFound = true; + + // Notify the waiting thread + synchronized (mLock) { + mLock.notifyAll(); + } + } + } + +} diff --git a/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerLollipop.java b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerLollipop.java new file mode 100644 index 0000000..b8d20c9 --- /dev/null +++ b/src/no/nordicsemi/android/dfu/scanner/BootloaderScannerLollipop.java @@ -0,0 +1,89 @@ +package no.nordicsemi.android.dfu.scanner; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.os.Build; +import android.os.Handler; + +/** + * @see BootloaderScanner + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class BootloaderScannerLollipop extends ScanCallback implements BootloaderScanner { + private Object mLock = new Object(); + private String mDeviceAddress; + private String mDeviceAddressIncremented; + private String mBootloaderAddress; + private boolean mFound; + + @Override + public String searchFor(final String deviceAddress) { + final String fistBytes = deviceAddress.substring(0, 15); + final String lastByte = deviceAddress.substring(15); // assuming that the device address is correct + final String lastByteIncremented = String.format("%02X", (Integer.valueOf(lastByte, 16) + ADDRESS_DIFF) & 0xFF); + + mDeviceAddress = deviceAddress; + mDeviceAddressIncremented = fistBytes + lastByteIncremented; + mBootloaderAddress = null; + mFound = false; + + // Add timeout + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (mFound) + return; + + mBootloaderAddress = null; + mFound = true; + + // Notify the waiting thread + synchronized (mLock) { + mLock.notifyAll(); + } + } + }, BootloaderScanner.TIMEOUT); + + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); + final List