diff --git a/README.md b/README.md index 9d34b8c..7951381 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,14 @@ ### Usage -The compat library may be found on jcenter and Maven Central repository. Add it to your project by adding the following dependency: +The DFU library may be found on jcenter and Maven Central repository. Add it to your project by +adding the following dependency: + +```Groovy +implementation 'no.nordicsemi.android:dfu:1.9.0' +``` + +For projects not migrated to Android Jetpack, use: ```Groovy implementation 'no.nordicsemi.android:dfu:1.8.1' @@ -13,18 +20,28 @@ implementation 'no.nordicsemi.android:dfu:1.8.1' If you use proguard, add the following line to your proguard rules: ```-keep class no.nordicsemi.android.dfu.** { *; }``` +Starting from version 1.9.0 the library is able to retry a DFU update in case of an unwanted +disconnection. However, to maintain backward compatibility, this feature is by default disabled. +Call `initiator.setNumberOfRetries(int)` to set how many attempts the service should perform. +Secure DFU will be resumed after it has been interrupted from the point it stopped, while the +Legacy DFU will start again. + ### Device Firmware Update (DFU) -The nRF5x Series chips are flash-based SoCs, and as such they represent the most flexible solution available. A key feature of the nRF5x Series and their associated software architecture -and S-Series SoftDevices is the possibility for Over-The-Air Device Firmware Upgrade (OTA-DFU). See Figure 1. OTA-DFU allows firmware upgrades to be issued and downloaded to products -in the field via the cloud and so enables OEMs to fix bugs and introduce new features to products that are already out on the market. +The nRF5x Series chips are flash-based SoCs, and as such they represent the most flexible solution available. +A key feature of the nRF5x Series and their associated software architecture and S-Series SoftDevices +is the possibility for Over-The-Air Device Firmware Upgrade (OTA-DFU). See Figure 1. +OTA-DFU allows firmware upgrades to be issued and downloaded to products in the field via the cloud +and so enables OEMs to fix bugs and introduce new features to products that are already out on the market. This brings added security and flexibility to product development when using the nRF5x Series SoCs. ![Device Firmware Update](resources/dfu.png) -This repository contains a tested library for Android 4.3+ platform which may be used to perform Device Firmware Update on the nRF5x device using a phone or a tablet. +This repository contains a tested library for Android 4.3+ platform which may be used to perform +Device Firmware Update on the nRF5x device using a phone or a tablet. -DFU library has been designed to make it very easy to include these devices into your application. It is compatible with all Bootloader/DFU versions. +DFU library has been designed to make it very easy to include these devices into your application. +It is compatible with all Bootloader/DFU versions. [![Alt text for your video](http://img.youtube.com/vi/LdY2m_bZTgE/0.jpg)](http://youtu.be/LdY2m_bZTgE) @@ -34,38 +51,50 @@ See the [documentation](documentation) for more information. ### Requirements -The library is compatible with nRF51 and nRF52 devices with S-Series Soft Device and the DFU Bootloader flashed on. +The library is compatible with nRF51 and nRF52 devices with S-Series Soft Device and the +DFU Bootloader flashed on. ### DFU History #### Legacy DFU * **SDK 4.3.0** - First version of DFU over Bluetooth Smart. DFU supports Application update. -* **SDK 6.1.0** - DFU Bootloader supports Soft Device and Bootloader update. As the updated Bootloader may be dependent on the new Soft Device, those two may be sent and installed together. +* **SDK 6.1.0** - DFU Bootloader supports Soft Device and Bootloader update. As the updated + Bootloader may be dependent on the new Soft Device, those two may be sent and + installed together. - Buttonless update support for non-bonded devices. -* **SDK 7.0.0** - The extended init packet is required. The init packet contains additional validation information: device type and revision, application version, compatible Soft Devices and the firmware CRC. -* **SDK 8.0.0** - The bond information may be preserved after an application update. The new application, when first started, will send the Service Change indication to the phone to refresh the services. +* **SDK 7.0.0** - The extended init packet is required. The init packet contains additional + validation information: device type and revision, application version, compatible + Soft Devices and the firmware CRC. +* **SDK 8.0.0** - The bond information may be preserved after an application update. + The new application, when first started, will send the Service Change indication + to the phone to refresh the services. - Buttonless update support for bonded devices - sharing the LTK between an app and the bootloader. #### Secure DFU * **SDK 12.0.0** - New Secure DFU has been released. Buttonless service is experimental. -* **SDK 13.0.0** - Buttonless DFU (still experimental) uses different UUIDs. No bond sharing supported. Bootloader will use address +1. -* **SDK 14.0.0** - Buttonless DFU is no longer experimental. A new UUID (0004) added for bonded only devices (previous one (0003) is for non-bonded only). +* **SDK 13.0.0** - Buttonless DFU (still experimental) uses different UUIDs. No bond sharing + supported. Bootloader will use address +1. +* **SDK 14.0.0** - Buttonless DFU is no longer experimental. A new UUID (0004) added for bonded + only devices (previous one (0003) is for non-bonded only). * **SDK 15.0.0** - Support for higher MTUs added. This library is fully backwards compatible and supports both the new and legacy DFU. -The experimental buttonless DFU service from SDK 12 is supported since version 1.1.0. Due to the fact, that this experimental service from SDK 12 is not safe, -you have to call [starter.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)](https://github.com/NordicSemiconductor/Android-DFU-Library/blob/release/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceInitiator.java#L376) -to enable it. Read the method documentation for details. It is recommended to use the Buttonless service from SDK 13 (for non-bonded devices, or 14 for bonded). +The experimental buttonless DFU service from SDK 12 is supported since version 1.1.0. +Due to the fact, that this experimental service from SDK 12 is not safe, you have to call +[starter.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)](https://github.com/NordicSemiconductor/Android-DFU-Library/blob/release/dfu/src/main/java/no/nordicsemi/android/dfu/DfuServiceInitiator.java#L376) +to enable it. Read the method documentation for details. It is recommended to use the Buttonless +service from SDK 13 (for non-bonded devices, or 14 for bonded). Both are supported since DFU Library 1.3.0. Check platform folders for mode details about compatibility for each library. ### React Native -A library for both iOS and Android that is based on this library is available for React Native: [react-native-nordic-dfu](https://github.com/Pilloxa/react-native-nordic-dfu) +A library for both iOS and Android that is based on this library is available for React Native: +[react-native-nordic-dfu](https://github.com/Pilloxa/react-native-nordic-dfu) ### Resources diff --git a/build.gradle b/build.gradle index ca95869..c4150c2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' diff --git a/dfu/build.gradle b/dfu/build.gradle index 99c6fd5..a6be985 100644 --- a/dfu/build.gradle +++ b/dfu/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.jfrog.bintray' ext { PUBLISH_GROUP_ID = 'no.nordicsemi.android' PUBLISH_ARTIFACT_ID = 'dfu' - PUBLISH_VERSION = '1.8.1' + PUBLISH_VERSION = '1.9.0' bintrayRepo = 'android' bintrayName = 'dfu-library' @@ -45,8 +45,8 @@ android { defaultConfig { minSdkVersion 18 targetSdkVersion 28 - versionCode 22 - versionName "1.8.1" + versionCode 23 + versionName "1.9.0" } buildTypes { @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.core:core:1.1.0-alpha02' + implementation 'androidx.core:core:1.1.0-alpha04' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.annotation:annotation:1.0.1' implementation 'com.google.code.gson:gson:2.8.5' diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseButtonlessDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseButtonlessDfuImpl.java index b8f6cab..055f07f 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseButtonlessDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseButtonlessDfuImpl.java @@ -27,6 +27,8 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; +import androidx.annotation.NonNull; + /** * A base implementation of a buttonless service. The purpose of a buttonless service is to * switch a device into the DFU bootloader mode. @@ -51,7 +53,7 @@ import android.content.Intent; } } - BaseButtonlessDfuImpl(final Intent intent, final DfuBaseService service) { + BaseButtonlessDfuImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) { super(intent, service); } @@ -68,7 +70,8 @@ import android.content.Intent; * @param forceRefresh true, if cache should be cleared even for a bonded device. Usually the Service Changed indication should be used for this purpose. * @param scanForBootloader true to scan for advertising bootloader, false to keep the same address */ - protected void finalize(final Intent intent, final boolean forceRefresh, final boolean scanForBootloader) { + @SuppressWarnings("SameParameterValue") + void finalize(@NonNull final Intent intent, final boolean forceRefresh, final boolean scanForBootloader) { /* * We are done with DFU. Now the service may refresh device cache and clear stored services. * For bonded device this is required only if if doesn't support Service Changed indication. diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java index 3b8ca39..99eae3f 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseCustomDfuImpl.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.util.UUID; import java.util.zip.CRC32; +import androidx.annotation.NonNull; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; import no.nordicsemi.android.dfu.internal.exception.HexFileValidationException; @@ -47,38 +48,43 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; /** * Flag indicating whether the firmware is being transmitted or not. */ - protected boolean mFirmwareUploadInProgress; + boolean mFirmwareUploadInProgress; /** - * The number of packets of firmware data to be send before receiving a new Packets receipt notification. 0 disables the packets notifications. + * The number of packets of firmware data to be send before receiving a new Packets + * receipt notification. 0 disables the packets notifications. */ - protected int mPacketsBeforeNotification; + int mPacketsBeforeNotification; /** * The number of packets sent since last notification. */ - protected int mPacketsSentSinceNotification; + private int mPacketsSentSinceNotification; /** *

- * Flag set to true when the DFU target had send a notification with status other than success. Setting it to true will abort sending firmware and + * Flag set to true when the DFU target had send a notification with status other + * than success. Setting it to true will abort sending firmware and * stop logging notifications (read below for explanation). - *

*

- * The onCharacteristicWrite(..) callback is called when Android writes the packet into the outgoing queue, not when it physically sends the data. - * This means that the service will first put up to N* packets, one by one, to the queue, while in fact the first one is transmitted. - * In case the DFU target is in an invalid state it will notify Android with a notification 10-03-02 for each packet of firmware that has been sent. - * After receiving the first such notification, the DFU service will add the reset command to the outgoing queue, but it will still be receiving such notifications - * until all the data packets are sent. Those notifications should be ignored. This flag will prevent from logging "Notification received..." more than once. - *

+ * The onCharacteristicWrite(..) callback is called when Android writes the packet into the + * outgoing queue, not when it physically sends the data. This means that the service will + * first put up to N* packets, one by one, to the queue, while in fact the first one is transmitted. + * In case the DFU target is in an invalid state it will notify Android with a notification + * 10-03-02 for each packet of firmware that has been sent. After receiving the first such + * notification, the DFU service will add the reset command to the outgoing queue, + * but it will still be receiving such notifications until all the data packets are sent. + * Those notifications should be ignored. This flag will prevent from logging + * "Notification received..." more than once. *

- * Additionally, sometimes after writing the command 6 ({@link LegacyDfuImpl#OP_CODE_RESET}), Android will receive a notification and update the characteristic value with 10-03-02 and the callback for write - * reset command will log "[DFU] Data written to ..., value (0x): 10-03-02" instead of "...(x0): 06". But this does not matter for the DFU process. - *

+ * Additionally, sometimes after writing the command 6 ({@link LegacyDfuImpl#OP_CODE_RESET}), + * Android will receive a notification and update the characteristic value with 10-03-02 and + * the callback for write reset command will log + * "[DFU] Data written to ..., value (0x): 10-03-02" instead of "...(x0): 06". + * But this does not matter for the DFU process. *

* N* - Value of Packet Receipt Notification, 12 by default. - *

*/ - protected boolean mRemoteErrorOccurred; + boolean mRemoteErrorOccurred; - protected class BaseCustomBluetoothCallback extends BaseBluetoothGattCallback { + class BaseCustomBluetoothCallback extends BaseBluetoothGattCallback { protected void onPacketCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { // this method can be overwritten on the final class } @@ -88,7 +94,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; if (status == BluetoothGatt.GATT_SUCCESS) { /* * This method is called when either a CONTROL POINT or PACKET characteristic has been written. - * If it is the CONTROL POINT characteristic, just set the {@link mRequestCompleted} flag to true. The main thread will continue its task when notified. + * If it is the CONTROL POINT characteristic, just set the {@link mRequestCompleted} + * flag to true. The main thread will continue its task when notified. * If the PACKET characteristic was written we must: * - if the image size was written in DFU Start procedure, just set flag to true * otherwise @@ -98,7 +105,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; if (characteristic.getUuid().equals(getPacketCharacteristicUUID())) { if (mInitPacketInProgress) { // We've got confirmation that the init packet was sent - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); mInitPacketInProgress = false; } else if (mFirmwareUploadInProgress) { // If the PACKET characteristic was written with image data, update counters @@ -152,14 +160,18 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; onPacketCharacteristicWrite(gatt, characteristic, status); } } else { - // If the CONTROL POINT characteristic was written just set the flag to true. The main thread will continue its task when notified. - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); + // If the CONTROL POINT characteristic was written just set the flag to true. + // The main thread will continue its task when notified. + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Data written to " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); mRequestCompleted = true; } } else { /* - * If a Reset (Op Code = 6) or Activate and Reset (Op Code = 5) commands are sent, the DFU target resets and sometimes does it so quickly that does not manage to send - * any ACK to the controller and error 133 is thrown here. This bug should be fixed in SDK 8.0+ where the target would gracefully disconnect before restarting. + * If a Reset (Op Code = 6) or Activate and Reset (Op Code = 5) commands are sent, + * the DFU target resets and sometimes does it so quickly that does not manage to send + * any ACK to the controller and error 133 is thrown here. This bug should be fixed + * in SDK 8.0+ where the target would gracefully disconnect before restarting. */ if (mResetRequestSent) mRequestCompleted = true; @@ -171,7 +183,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; notifyLock(); } - protected void handlePacketReceiptNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + void handlePacketReceiptNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { // Secure DFU: // When PRN is set to be received after the object is complete we don't want to send anything. First the object needs to be executed. if (!mFirmwareUploadInProgress) { @@ -179,7 +191,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; return; } - final BluetoothGattCharacteristic packetCharacteristic = gatt.getService(getDfuServiceUUID()).getCharacteristic(getPacketCharacteristicUUID()); + final BluetoothGattCharacteristic packetCharacteristic = + gatt.getService(getDfuServiceUUID()).getCharacteristic(getPacketCharacteristicUUID()); try { mPacketsSentSinceNotification = 0; @@ -216,19 +229,23 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } } - protected void handleNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Notification received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); + @SuppressWarnings("unused") + void handleNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Notification received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); mReceivedData = characteristic.getValue(); mFirmwareUploadInProgress = false; } } - BaseCustomDfuImpl(final Intent intent, final DfuBaseService service) { + @SuppressWarnings("deprecation") + BaseCustomDfuImpl(@NonNull final Intent intent, final DfuBaseService service) { super(intent, service); if (intent.hasExtra(DfuBaseService.EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED)) { // Read from intent - final boolean packetReceiptNotificationEnabled = intent.getBooleanExtra(DfuBaseService.EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M); + final boolean packetReceiptNotificationEnabled = + intent.getBooleanExtra(DfuBaseService.EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M); int numberOfPackets = intent.getIntExtra(DfuBaseService.EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE, DfuServiceInitiator.DEFAULT_PRN_VALUE); if (numberOfPackets < 0 || numberOfPackets > 0xFFFF) numberOfPackets = DfuServiceInitiator.DEFAULT_PRN_VALUE; @@ -238,10 +255,12 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } else { // Read preferences final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service); - final boolean packetReceiptNotificationEnabled = preferences.getBoolean(DfuSettingsConstants.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M); + final boolean packetReceiptNotificationEnabled = + preferences.getBoolean(DfuSettingsConstants.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M); String value = preferences.getString(DfuSettingsConstants.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(DfuServiceInitiator.DEFAULT_PRN_VALUE)); int numberOfPackets; try { + //noinspection ConstantConditions numberOfPackets = Integer.parseInt(value); if (numberOfPackets < 0 || numberOfPackets > 0xFFFF) numberOfPackets = DfuServiceInitiator.DEFAULT_PRN_VALUE; @@ -254,6 +273,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } } + @SuppressWarnings("unused") protected abstract UUID getControlPointCharacteristicUUID(); protected abstract UUID getPacketCharacteristicUUID(); @@ -262,13 +282,16 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; /** * Wends the whole init packet stream to the given characteristic. + * * @param characteristic the target characteristic * @param crc32 the CRC object to be updated based on the data sent - * @throws DfuException - * @throws DeviceDisconnectedException - * @throws UploadAbortedException + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of + * the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - protected void writeInitData(final BluetoothGattCharacteristic characteristic, final CRC32 crc32) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + void writeInitData(final BluetoothGattCharacteristic characteristic, final CRC32 crc32) + throws DfuException, DeviceDisconnectedException, UploadAbortedException { try { byte[] data = mBuffer; int size; @@ -284,18 +307,20 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } /** - * Writes the Init packet to the characteristic. This method is SYNCHRONOUS and wait until the {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} - * will be called or the device gets disconnected. If connection state will change, or an error will occur, an exception will be thrown. + * Writes the Init packet to the characteristic. This method is SYNCHRONOUS and wait until the + * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} + * will be called or the device gets disconnected. If connection state will change, + * or an error will occur, an exception will be thrown. * - * @param characteristic the characteristic to write to. Should be the DFU PACKET - * @param buffer the init packet as a byte array. This must be shorter or equal to 20 bytes (TODO check this restriction). - * @param size the init packet size - * @throws DeviceDisconnectedException - * @throws DfuException - * @throws UploadAbortedException + * @param characteristic the characteristic to write to. Should be the DFU PACKET. + * @param buffer the init packet as a byte array. + * @param size the init packet size. + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - private void writeInitPacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) throws DeviceDisconnectedException, DfuException, - UploadAbortedException { + private void writeInitPacket(final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) + throws DeviceDisconnectedException, DfuException, UploadAbortedException { if (mAborted) throw new UploadAbortedException(); byte[] locBuffer = buffer; @@ -323,23 +348,25 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } - if (mError != 0) - throw new DfuException("Unable to write Init DFU Parameters", mError); if (!mConnected) throw new DeviceDisconnectedException("Unable to write Init DFU Parameters: device disconnected"); + if (mError != 0) + throw new DfuException("Unable to write Init DFU Parameters", mError); } /** - * Starts sending the data. This method is SYNCHRONOUS and terminates when the whole file will be uploaded or the device get disconnected. - * If connection state will change, or an error will occur, an exception will be thrown. + * Starts sending the data. This method is SYNCHRONOUS and terminates when the whole file will + * be uploaded or the device get disconnected. If connection state will change, or an error + * will occur, an exception will be thrown. * - * @param packetCharacteristic the characteristic to write file content to. Must be the DFU PACKET - * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of the transmission. - * @throws DfuException Thrown if DFU error occur - * @throws UploadAbortedException + * @param packetCharacteristic the characteristic to write file content to. Must be the DFU PACKET. + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle + * of the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - protected void uploadFirmwareImage(final BluetoothGattCharacteristic packetCharacteristic) throws DeviceDisconnectedException, - DfuException, UploadAbortedException { + void uploadFirmwareImage(final BluetoothGattCharacteristic packetCharacteristic) + throws DeviceDisconnectedException, DfuException, UploadAbortedException { if (mAborted) throw new UploadAbortedException(); mReceivedData = null; @@ -350,7 +377,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; final byte[] buffer = mBuffer; try { final int size = mFirmwareStream.read(buffer); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Sending firmware to characteristic " + packetCharacteristic.getUuid() + "..."); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, + "Sending firmware to characteristic " + packetCharacteristic.getUuid() + "..."); writePacket(mGatt, packetCharacteristic, buffer, size); } catch (final HexFileValidationException e) { throw new DfuException("HEX file not valid", DfuBaseService.ERROR_FILE_INVALID); @@ -367,18 +395,19 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; loge("Sleeping interrupted", e); } - if (mError != 0) - throw new DfuException("Uploading Firmware Image failed", mError); if (!mConnected) throw new DeviceDisconnectedException("Uploading Firmware Image failed: device disconnected"); + if (mError != 0) + throw new DfuException("Uploading Firmware Image failed", mError); } /** - * Writes the buffer to the characteristic. The maximum size of the buffer is 20 bytes. This method is ASYNCHRONOUS and returns immediately after adding the data to TX queue. + * Writes the buffer to the characteristic. The maximum size of the buffer is dependent on MTU. + * This method is ASYNCHRONOUS and returns immediately after adding the data to TX queue. * - * @param characteristic the characteristic to write to. Should be the DFU PACKET - * @param buffer the buffer with 1-20 bytes - * @param size the number of bytes from the buffer to send + * @param characteristic the characteristic to write to. Should be the DFU PACKET. + * @param buffer the buffer with 1-20 bytes. + * @param size the number of bytes from the buffer to send. */ private void writePacket(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] buffer, final int size) { byte[] locBuffer = buffer; @@ -391,25 +420,24 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); characteristic.setValue(locBuffer); gatt.writeCharacteristic(characteristic); - // FIXME BLE buffer overflow - // after writing to the device with WRITE_NO_RESPONSE property the onCharacteristicWrite callback is received immediately after writing data to a buffer. - // The real sending is much slower than adding to the buffer. This method does not return false if writing didn't succeed.. just the callback is not invoked. - // - // More info: this works fine on Nexus 5 (Android 4.4) (4.3 seconds) and on Samsung S4 (Android 4.3) (20 seconds) so this is a driver issue. - // Nexus 4 and 7 uses Qualcomm chip, Nexus 5 and Samsung uses Broadcom chips. } /** - * Closes the BLE connection to the device and removes/restores bonding, if a proper flags were set in the {@link DfuServiceInitiator}. - * This method will also change the DFU state to completed or restart the service to send the second part. - * @param intent the intent used to start the DFU service. It contains all user flags in the bundle. - * @param forceRefresh true, if cache should be cleared even for a bonded device. Usually the Service Changed indication should be used for this purpose. + * Closes the BLE connection to the device and removes/restores bonding, if a proper flags were + * set in the {@link DfuServiceInitiator}. This method will also change the DFU state to + * completed or restart the service to send the second part. + * + * @param intent the intent used to start the DFU service. + * It contains all user flags in the bundle. + * @param forceRefresh true, if cache should be cleared even for a bonded device. + * Usually the Service Changed indication should be used for this purpose. */ - protected void finalize(final Intent intent, final boolean forceRefresh) { + void finalize(final Intent intent, final boolean forceRefresh) { /* * We are done with DFU. Now the service may refresh device cache and clear stored services. * For bonded device this is required only if if doesn't support Service Changed indication. - * Android shouldn't cache services of non-bonded devices having Service Changed characteristic in their database, but it does, so... + * Android shouldn't cache services of non-bonded devices having Service Changed + * characteristic in their database, but it does, so... */ final boolean keepBond = intent.getBooleanExtra(DfuBaseService.EXTRA_KEEP_BOND, false); mService.refreshDeviceCache(mGatt, forceRefresh || !keepBond); @@ -429,7 +457,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; // The bond information was lost. removeBond(); - // Give some time for removing the bond information. 300ms was to short, let's set it to 2 seconds just to be sure. + // Give some time for removing the bond information. 300 ms was to short, + // let's set it to 2 seconds just to be sure. mService.waitFor(2000); alreadyWaited = true; } @@ -443,8 +472,10 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; /* * We need to send PROGRESS_COMPLETED message only when all files has been transmitted. - * In case you want to send the Soft Device and/or Bootloader and the Application, the service will be started twice: one to send SD+BL, and the - * second time to send the Application only (using the new Bootloader). In the first case we do not send PROGRESS_COMPLETED notification. + * In case you want to send the Soft Device and/or Bootloader and the Application, + * the service will be started twice: one to send SD+BL, and the second time to send the + * Application only (using the new Bootloader). + * In the first case we do not send PROGRESS_COMPLETED notification. */ if (mProgressInfo.isLastPart()) { // Delay this event a little bit. Android needs some time to prepare for reconnection. @@ -453,17 +484,24 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; mProgressInfo.setProgress(DfuBaseService.PROGRESS_COMPLETED); } else { /* - * In case when the SoftDevice has been upgraded, and the application should be send in the following connection, we have to - * make sure that we know the address the device is advertising with. Depending on the method used to start the DFU bootloader the first time + * In case when the SoftDevice has been upgraded, and the application should be send + * in the following connection, we have to make sure that we know the address the device + * is advertising with. Depending on the method used to start the DFU bootloader the first time * the new Bootloader may advertise with the same address or one incremented by 1. - * When the buttonless update was used, the bootloader will use the same address as the application. The cached list of services on the Android device - * should be cleared thanks to the Service Changed characteristic (the fact that it exists if not bonded, or the Service Changed indication on bonded one). - * In case of forced DFU mode (using a button), the Bootloader does not know whether there was the Service Changed characteristic present in the list of - * application's services so it must advertise with a different address. The same situation applies when the new Soft Device was uploaded and the old - * application has been removed in this process. + * When the buttonless update was used, the bootloader will use the same address as the + * application. The cached list of services on the Android device should be cleared + * thanks to the Service Changed characteristic (the fact that it exists if not bonded, + * or the Service Changed indication on bonded one). + * In case of forced DFU mode (using a button), the Bootloader does not know whether + * there was the Service Changed characteristic present in the list of application's + * services so it must advertise with a different address. The same situation applies + * when the new Soft Device was uploaded and the old application has been removed in + * this process. * - * We could have save the fact of jumping as a parameter of the service but it ma be that some Android devices must first scan a device before connecting to it. - * It a device with the address+1 has never been detected before the service could have failed on connection. + * We could have save the fact of jumping as a parameter of the service but it ma be + * that some Android devices must first scan a device before connecting to it. + * It a device with the address+1 has never been detected before the service could have + * failed on connection. */ /* diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java index 9d86e13..70ea361 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/BaseDfuImpl.java @@ -22,7 +22,6 @@ package no.nordicsemi.android.dfu; -import android.annotation.SuppressLint; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -30,13 +29,16 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.os.Build; -import androidx.annotation.RequiresApi; import android.util.Log; import java.io.InputStream; import java.lang.reflect.Method; import java.util.UUID; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import no.nordicsemi.android.dfu.internal.ArchiveInputStream; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; @@ -46,59 +48,72 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /* package */ abstract class BaseDfuImpl implements DfuService { private static final String TAG = "DfuImpl"; - protected static final UUID GENERIC_ATTRIBUTE_SERVICE_UUID = new UUID(0x0000180100001000L, 0x800000805F9B34FBL); - protected static final UUID SERVICE_CHANGED_UUID = new UUID(0x00002A0500001000L, 0x800000805F9B34FBL); - protected static final UUID CLIENT_CHARACTERISTIC_CONFIG = new UUID(0x0000290200001000L, 0x800000805f9b34fbL); - protected static final int NOTIFICATIONS = 1; - protected static final int INDICATIONS = 2; + static final UUID GENERIC_ATTRIBUTE_SERVICE_UUID = new UUID(0x0000180100001000L, 0x800000805F9B34FBL); + static final UUID SERVICE_CHANGED_UUID = new UUID(0x00002A0500001000L, 0x800000805F9B34FBL); + static final UUID CLIENT_CHARACTERISTIC_CONFIG = new UUID(0x0000290200001000L, 0x800000805f9b34fbL); + static final int NOTIFICATIONS = 1; + static final int INDICATIONS = 2; - protected static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - protected static final int MAX_PACKET_SIZE_DEFAULT = 20; // the default maximum number of bytes in one packet is 20. + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static final int MAX_PACKET_SIZE_DEFAULT = 20; // the default maximum number of bytes in one packet is 20. /** - * Lock used in synchronization purposes + * Lock used in synchronization purposes. */ - protected final Object mLock = new Object(); + final Object mLock = new Object(); - protected InputStream mFirmwareStream; - protected InputStream mInitPacketStream; + InputStream mFirmwareStream; + InputStream mInitPacketStream; - /** The target GATT device. */ - protected BluetoothGatt mGatt; - /** The firmware type. See TYPE_* constants. */ - protected int mFileType; - /** Flag set to true if sending was paused. */ - protected boolean mPaused; - /** Flag set to true if sending was aborted. */ - protected boolean mAborted; - /** Flag indicating whether the device is still connected. */ - protected boolean mConnected; + /** + * The target GATT device. + */ + BluetoothGatt mGatt; + /** + * The firmware type. See TYPE_* constants. + */ + int mFileType; + /** + * Flag set to true if sending was paused. + */ + boolean mPaused; + /** + * Flag set to true if sending was aborted. + */ + boolean mAborted; + /** + * Flag indicating whether the device is still connected. + */ + boolean mConnected; /** * Flag indicating whether the request was completed or not */ - protected boolean mRequestCompleted; + boolean mRequestCompleted; /** - * Flag sent when a request has been sent that will cause the DFU target to reset. Often, after sending such command, Android throws a connection state error. If this flag is set the error will be - * ignored. + * Flag sent when a request has been sent that will cause the DFU target to reset. + * Often, after sending such command, Android throws a connection state error. + * If this flag is set the error will be ignored. */ - protected boolean mResetRequestSent; + boolean mResetRequestSent; /** - * The number of the last error that has occurred or 0 if there was no error + * The number of the last error that has occurred or 0 if there was no error. */ - protected int mError; + int mError; /** * Latest data received from device using notification. */ - protected byte[] mReceivedData = null; - protected byte[] mBuffer = new byte[MAX_PACKET_SIZE_DEFAULT]; - protected DfuBaseService mService; - protected DfuProgressInfo mProgressInfo; - protected int mImageSizeInBytes; - protected int mInitPacketSizeInBytes; + byte[] mReceivedData = null; + byte[] mBuffer = new byte[MAX_PACKET_SIZE_DEFAULT]; + DfuBaseService mService; + DfuProgressInfo mProgressInfo; + int mImageSizeInBytes; + int mInitPacketSizeInBytes; private int mCurrentMtu; protected class BaseBluetoothGattCallback extends DfuGattCallback { - // The Implementation object is created depending on device services, so after the device is connected and services were scanned. + // The Implementation object is created depending on device services, so after the device + // is connected and services were scanned. + // public void onConnected() { } @Override @@ -113,7 +128,8 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /* * This method is called when the DFU Version characteristic has been read. */ - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Read Response received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Read Response received from " + characteristic.getUuid() + ", value (0x): " + parse(characteristic)); mReceivedData = characteristic.getValue(); mRequestCompleted = true; } else { @@ -127,7 +143,8 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; public void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (CLIENT_CHARACTERISTIC_CONFIG.equals(descriptor.getUuid())) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Read Response received from descr." + descriptor.getCharacteristic().getUuid() + ", value (0x): " + parse(descriptor)); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Read Response received from descr." + descriptor.getCharacteristic().getUuid() + ", value (0x): " + parse(descriptor)); if (SERVICE_CHANGED_UUID.equals(descriptor.getCharacteristic().getUuid())) { // We have enabled indications for the Service Changed characteristic mRequestCompleted = true; @@ -147,13 +164,16 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; public void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (CLIENT_CHARACTERISTIC_CONFIG.equals(descriptor.getUuid())) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to descr." + descriptor.getCharacteristic().getUuid() + ", value (0x): " + parse(descriptor)); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "Data written to descr." + descriptor.getCharacteristic().getUuid() + ", value (0x): " + parse(descriptor)); if (SERVICE_CHANGED_UUID.equals(descriptor.getCharacteristic().getUuid())) { // We have enabled indications for the Service Changed characteristic - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Indications enabled for " + descriptor.getCharacteristic().getUuid()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, + "Indications enabled for " + descriptor.getCharacteristic().getUuid()); } else { // We have enabled notifications for this characteristic - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Notifications enabled for " + descriptor.getCharacteristic().getUuid()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, + "Notifications enabled for " + descriptor.getCharacteristic().getUuid()); } } } else { @@ -165,7 +185,7 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; @Override public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { + if (status == BluetoothGatt.GATT_SUCCESS) { mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "MTU changed to: " + mtu); if (mtu - 3 > mBuffer.length) mBuffer = new byte[mtu - 3]; // Maximum payload size is MTU - 3 bytes @@ -183,8 +203,9 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; @Override public void onPhyUpdate(final BluetoothGatt gatt, final int txPhy, final int rxPhy, final int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "PHY updated (TX: " + phyToString(txPhy) + ", RX: " + phyToString(rxPhy) + ")"); + if (status == BluetoothGatt.GATT_SUCCESS) { + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, + "PHY updated (TX: " + phyToString(txPhy) + ", RX: " + phyToString(rxPhy) + ")"); logi("PHY updated (TX: " + phyToString(txPhy) + ", RX: " + phyToString(rxPhy) + ")"); } else { logw("Updating PHY failed: " + status + " (txPhy: " + txPhy + ", rxPhy: " + rxPhy + ")"); @@ -231,7 +252,8 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } } - /* package */ BaseDfuImpl(final Intent intent, final DfuBaseService service) { + @SuppressWarnings("unused") + BaseDfuImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) { mService = service; mProgressInfo = service.mProgressInfo; mConnected = true; // the device is connected when impl object it created @@ -267,7 +289,11 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } @Override - public boolean initialize(final Intent intent, final BluetoothGatt gatt, final int fileType, final InputStream firmwareStream, final InputStream initPacketStream) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + public boolean initialize(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt, + final int fileType, + @NonNull final InputStream firmwareStream, + @Nullable final InputStream initPacketStream) + throws DfuException, DeviceDisconnectedException, UploadAbortedException { mGatt = gatt; mFileType = fileType; mFirmwareStream = firmwareStream; @@ -293,14 +319,16 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Sending application"); } - int size; + int size = 0; try { - if (initPacketStream.markSupported()) { - initPacketStream.reset(); - } - size = initPacketStream.available(); + if (initPacketStream != null) { + if (initPacketStream.markSupported()) { + initPacketStream.reset(); + } + size = initPacketStream.available(); + } } catch (final Exception e) { - size = 0; + // ignore } mInitPacketSizeInBytes = size; try { @@ -350,14 +378,14 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; return true; } - protected void notifyLock() { + void notifyLock() { // Notify waiting thread synchronized (mLock) { mLock.notifyAll(); } } - protected void waitIfPaused() { + void waitIfPaused() { try { synchronized (mLock) { while (mPaused) @@ -369,16 +397,21 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } /** - * Enables or disables the notifications for given characteristic. This method is SYNCHRONOUS and wait until the - * {@link android.bluetooth.BluetoothGattCallback#onDescriptorWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattDescriptor, int)} will be called or the device gets disconnected. + * Enables or disables the notifications for given characteristic. + * This method is SYNCHRONOUS and wait until the + * {@link android.bluetooth.BluetoothGattCallback#onDescriptorWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattDescriptor, int)} + * will be called or the device gets disconnected. * If connection state will change, or an error will occur, an exception will be thrown. * - * @param characteristic the characteristic to enable or disable notifications for - * @param type {@link #NOTIFICATIONS} or {@link #INDICATIONS} - * @throws DfuException - * @throws UploadAbortedException + * @param characteristic the characteristic to enable or disable notifications for. + * @param type {@link #NOTIFICATIONS} or {@link #INDICATIONS}. + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of + * the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - protected void enableCCCD(final BluetoothGattCharacteristic characteristic, final int type) throws DeviceDisconnectedException, DfuException, UploadAbortedException { + void enableCCCD(@NonNull final BluetoothGattCharacteristic characteristic, final int type) + throws DeviceDisconnectedException, DfuException, UploadAbortedException { final BluetoothGatt gatt = mGatt; final String debugString = type == NOTIFICATIONS ? "notifications" : "indications"; if (!mConnected) @@ -394,15 +427,18 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; return; logi("Enabling " + debugString + "..."); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Enabling " + debugString + " for " + characteristic.getUuid()); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, + "Enabling " + debugString + " for " + characteristic.getUuid()); // enable notifications locally - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, + "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)"); gatt.setCharacteristicNotification(characteristic, true); // enable notifications on the device descriptor.setValue(type == NOTIFICATIONS ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.writeDescriptor(" + descriptor.getUuid() + (type == NOTIFICATIONS ? ", value=0x01-00)" : ", value=0x02-00)")); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, + "gatt.writeDescriptor(" + descriptor.getUuid() + (type == NOTIFICATIONS ? ", value=0x01-00)" : ", value=0x02-00)")); gatt.writeDescriptor(descriptor); // We have to wait until device receives a response or an error occur @@ -417,21 +453,23 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } - if (mError != 0) - throw new DfuException("Unable to set " + debugString + " state", mError); if (!mConnected) throw new DeviceDisconnectedException("Unable to set " + debugString + " state: device disconnected"); + if (mError != 0) + throw new DfuException("Unable to set " + debugString + " state", mError); } /** * Reads the value of the Service Changed Client Characteristic Configuration descriptor (CCCD). * - * @return true if Service Changed CCCD is enabled and set to INDICATE - * @throws DeviceDisconnectedException - * @throws DfuException - * @throws UploadAbortedException + * @return true if Service Changed CCCD is enabled and set to INDICATE. + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of + * the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - private boolean isServiceChangedCCCDEnabled() throws DeviceDisconnectedException, DfuException, UploadAbortedException { + private boolean isServiceChangedCCCDEnabled() + throws DeviceDisconnectedException, DfuException, UploadAbortedException { if (!mConnected) throw new DeviceDisconnectedException("Unable to read Service Changed CCCD: device disconnected"); if (mAborted) @@ -468,10 +506,10 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } - if (mError != 0) - throw new DfuException("Unable to read Service Changed CCCD", mError); if (!mConnected) throw new DeviceDisconnectedException("Unable to read Service Changed CCCD: device disconnected"); + if (mError != 0) + throw new DfuException("Unable to read Service Changed CCCD", mError); // Return true if the CCCD value is return descriptor.getValue() != null && descriptor.getValue().length == 2 @@ -480,27 +518,32 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } /** - * Writes the operation code to the characteristic. This method is SYNCHRONOUS and wait until the - * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} will be called or - * the device gets disconnected. If connection state will change, or an error will occur, an exception will be thrown. + * Writes the operation code to the characteristic. + * This method is SYNCHRONOUS and wait until the + * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} + * will be called or the device gets disconnected. + * If connection state will change, or an error will occur, an exception will be thrown. * * @param characteristic the characteristic to write to. Should be the DFU CONTROL POINT * @param value the value to write to the characteristic * @param reset whether the command trigger restarting the device - * @throws DeviceDisconnectedException - * @throws DfuException - * @throws UploadAbortedException + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of + * the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - protected void writeOpCode(final BluetoothGattCharacteristic characteristic, final byte[] value, final boolean reset) throws DeviceDisconnectedException, DfuException, - UploadAbortedException { + void writeOpCode(@NonNull final BluetoothGattCharacteristic characteristic, @NonNull final byte[] value, final boolean reset) + throws DeviceDisconnectedException, DfuException, UploadAbortedException { if (mAborted) throw new UploadAbortedException(); mReceivedData = null; mError = 0; mRequestCompleted = false; /* - * Sending a command that will make the DFU target to reboot may cause an error 133 (0x85 - Gatt Error). If so, with this flag set, the error will not be shown to the user - * as the peripheral is disconnected anyway. See: mGattCallback#onCharacteristicWrite(...) method + * Sending a command that will make the DFU target to reboot may cause an error 133 + * (0x85 - Gatt Error). If so, with this flag set, the error will not be shown to the user + * as the peripheral is disconnected anyway. + * See: mGattCallback#onCharacteristicWrite(...) method */ mResetRequestSent = reset; @@ -519,10 +562,10 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } - if (!mResetRequestSent && mError != 0) - throw new DfuException("Unable to write Op Code " + value[0], mError); if (!mResetRequestSent && !mConnected) throw new DeviceDisconnectedException("Unable to write Op Code " + value[0] + ": device disconnected"); + if (!mResetRequestSent && mError != 0) + throw new DfuException("Unable to write Op Code " + value[0], mError); } /** @@ -530,8 +573,8 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; * * @return true if it's already bonded or the bonding has started */ - @SuppressLint("NewApi") - protected boolean createBond() { + @SuppressWarnings("UnusedReturnValue") + boolean createBond() { final BluetoothDevice device = mGatt.getDevice(); if (device.getBondState() == BluetoothDevice.BOND_BONDED) return true; @@ -565,16 +608,14 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; * @param device the target device * @return false if bonding failed (no hidden createBond() method in BluetoothDevice, or this method returned false */ - private boolean createBondApi18(final BluetoothDevice device) { + private boolean createBondApi18(@NonNull 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) { - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().createBond() (hidden)"); - return (Boolean) createBond.invoke(device); - } + mService.sendLogBroadcast(DfuBaseService.LOG_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); } @@ -584,9 +625,10 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /** * Removes the bond information for the given device. * - * @return true if operation succeeded, false otherwise + * @return true if operation succeeded, false otherwise */ - protected boolean removeBond() { + @SuppressWarnings("UnusedReturnValue") + boolean removeBond() { final BluetoothDevice device = mGatt.getDevice(); if (device.getBondState() == BluetoothDevice.BOND_NONE) return true; @@ -597,23 +639,21 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; * 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; - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().removeBond() (hidden)"); - result = (Boolean) removeBond.invoke(device); + //noinspection JavaReflectionMemberAccess + final Method removeBond = device.getClass().getMethod("removeBond"); + mRequestCompleted = false; + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().removeBond() (hidden)"); + result = (Boolean) removeBond.invoke(device); - // We have to wait until device is unbounded - try { - synchronized (mLock) { - while (!mRequestCompleted && !mAborted) - mLock.wait(); - } - } catch (final InterruptedException e) { - loge("Sleeping interrupted", e); - } - } - result = true; + // We have to wait until device is unbounded + try { + synchronized (mLock) { + while (!mRequestCompleted && !mAborted) + mLock.wait(); + } + } catch (final InterruptedException e) { + loge("Sleeping interrupted", e); + } } catch (final Exception e) { Log.w(TAG, "An exception occurred while removing bond information", e); } @@ -622,9 +662,10 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /** * Returns whether the device is bonded. + * * @return true if the device is bonded, false if not bonded or in process of bonding. */ - protected boolean isBonded() { + boolean isBonded() { final BluetoothDevice device = mGatt.getDevice(); return device.getBondState() == BluetoothDevice.BOND_BONDED; } @@ -632,10 +673,12 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; /** * Requests given MTU. This method is only supported on Android Lollipop or newer versions. * Only DFU from SDK 14.1 or newer supports MTU > 23. - * @param mtu new MTU to be requested + * + * @param mtu new MTU to be requested. */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - protected void requestMtu(final int mtu) throws DeviceDisconnectedException, UploadAbortedException { + void requestMtu(@IntRange(from = 0, to = 517) final int mtu) + throws DeviceDisconnectedException, UploadAbortedException { if (mAborted) throw new UploadAbortedException(); mRequestCompleted = false; @@ -659,15 +702,18 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } /** - * Waits until the notification will arrive. Returns the data returned by the notification. This method will block the thread until response is not ready or - * the device gets disconnected. If connection state will change, or an error will occur, an exception will be thrown. + * Waits until the notification will arrive. Returns the data returned by the notification. + * This method will block the thread until response is not ready or the device gets disconnected. + * If connection state will change, or an error will occur, an exception will be thrown. * * @return the value returned by the Control Point notification - * @throws DeviceDisconnectedException - * @throws DfuException - * @throws UploadAbortedException + * @throws DeviceDisconnectedException Thrown when the device will disconnect in the middle of + * the transmission. + * @throws DfuException Thrown if DFU error occur. + * @throws UploadAbortedException Thrown if DFU operation was aborted by user. */ - protected byte[] readNotificationResponse() throws DeviceDisconnectedException, DfuException, UploadAbortedException { + byte[] readNotificationResponse() + throws DeviceDisconnectedException, DfuException, UploadAbortedException { // do not clear the mReceiveData here. The response might already be obtained. Clear it in write request instead. try { synchronized (mLock) { @@ -679,20 +725,21 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; } if (mAborted) throw new UploadAbortedException(); - if (mError != 0) - throw new DfuException("Unable to write Op Code", mError); if (!mConnected) throw new DeviceDisconnectedException("Unable to write Op Code: device disconnected"); + if (mError != 0) + throw new DfuException("Unable to write Op Code", mError); return mReceivedData; } /** * Restarts the service based on the given intent. If parameter set this method will also scan for * an advertising bootloader that has address equal or incremented by 1 to the current one. - * @param intent the intent to be started as a service + * + * @param intent the intent to be started as a service * @param scanForBootloader true to scan for advertising bootloader, false to keep the same address */ - protected void restartService(final Intent intent, final boolean scanForBootloader) { + void restartService(@NonNull final Intent intent, final boolean scanForBootloader) { String newAddress = null; if (scanForBootloader) { mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Scanning for the DFU Bootloader..."); @@ -707,10 +754,14 @@ import no.nordicsemi.android.dfu.internal.scanner.BootloaderScannerFactory; if (newAddress != null) intent.putExtra(DfuBaseService.EXTRA_DEVICE_ADDRESS, newAddress); + + // Reset the DFU attempt counter + intent.putExtra(DfuBaseService.EXTRA_DFU_ATTEMPT, 0); + mService.startService(intent); } - protected String parse(final byte[] data) { + protected String parse(@Nullable final byte[] data) { if (data == null) return ""; diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuImpl.java index 65665d2..1c33304 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuImpl.java @@ -28,6 +28,7 @@ import android.content.Intent; import java.util.Locale; +import androidx.annotation.NonNull; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; import no.nordicsemi.android.dfu.internal.exception.RemoteDfuException; @@ -36,7 +37,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; import no.nordicsemi.android.error.SecureDfuError; /** - * A base class for buttonless service implementations made for Secure and in the future for Non-Secure DFU. + * A base class for buttonless service implementations made for Secure and in the future for + * Non-Secure DFU. */ /* package */ abstract class ButtonlessDfuImpl extends BaseButtonlessDfuImpl { @@ -44,34 +46,39 @@ import no.nordicsemi.android.error.SecureDfuError; private static final int OP_CODE_ENTER_BOOTLOADER_KEY = 0x01; private static final int OP_CODE_RESPONSE_CODE_KEY = 0x20; - private static final byte[] OP_CODE_ENTER_BOOTLOADER = new byte[] {OP_CODE_ENTER_BOOTLOADER_KEY}; + private static final byte[] OP_CODE_ENTER_BOOTLOADER = new byte[]{OP_CODE_ENTER_BOOTLOADER_KEY}; - ButtonlessDfuImpl(final Intent intent, final DfuBaseService service) { + ButtonlessDfuImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) { super(intent, service); } /** - * This method should return the type of the response received from the device after sending Enable Dfu command. - * Should be one of {@link #NOTIFICATIONS} or {@link #INDICATIONS}. - * @return response type + * This method should return the type of the response received from the device after sending + * Enable Dfu command. Should be one of {@link #NOTIFICATIONS} or {@link #INDICATIONS}. + * + * @return Response type. */ protected abstract int getResponseType(); /** * Returns the buttonless characteristic. - * @return the characteristic used to trigger buttonless jump to bootloader mode. + * + * @return The characteristic used to trigger buttonless jump to bootloader mode. */ protected abstract BluetoothGattCharacteristic getButtonlessDfuCharacteristic(); /** - * This method should return {@code true} if the bootloader is expected to start advertising with address - * incremented by 1. - * @return true if the bootloader may advertise with address +1, false if it will keep the same device address. + * This method should return {@code true} if the bootloader is expected to start advertising + * with address incremented by 1. + * + * @return True if the bootloader may advertise with address +1, false if it will keep + * the same device address. */ protected abstract boolean shouldScanForBootloader(); @Override - public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + public void performDfu(@NonNull final Intent intent) + throws DfuException, DeviceDisconnectedException, UploadAbortedException { mProgressInfo.setProgress(DfuBaseService.PROGRESS_STARTING); // Add one second delay to avoid the traffic jam before the DFU mode is enabled @@ -109,9 +116,10 @@ import no.nordicsemi.android.error.SecureDfuError; byte[] response; try { - // There may be a race condition here. The peripheral should send a notification and disconnect gracefully - // immediately after that, but the onConnectionStateChange event may be handled before this method ends. - // Also, sometimes the notification is not received at all. + // There may be a race condition here. The peripheral should send a notification + // and disconnect gracefully immediately after that, but the onConnectionStateChange + // event may be handled before this method ends. Also, sometimes the notification + // is not received at all. response = readNotificationResponse(); } catch (final DeviceDisconnectedException e) { // The device disconnect event was handled before the method finished, @@ -132,7 +140,8 @@ import no.nordicsemi.android.error.SecureDfuError; */ final int status = getStatusCode(response, OP_CODE_ENTER_BOOTLOADER_KEY); logi("Response received (Op Code = " + response[1] + ", Status = " + status + ")"); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Response received (Op Code = " + response[1] + ", Status = " + status + ")"); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, + "Response received (Op Code = " + response[1] + ", Status = " + status + ")"); if (status != DFU_STATUS_SUCCESS) throw new RemoteDfuException("Device returned error after sending Enter Bootloader", status); // The device will reset so we don't have to send Disconnect signal. @@ -152,7 +161,8 @@ import no.nordicsemi.android.error.SecureDfuError; } catch (final RemoteDfuException e) { final int error = DfuBaseService.ERROR_REMOTE_TYPE_SECURE_BUTTONLESS | e.getErrorNumber(); loge(e.getMessage()); - mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format(Locale.US, "Remote DFU error: %s", SecureDfuError.parseButtonlessError(error))); + mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format(Locale.US, + "Remote DFU error: %s", SecureDfuError.parseButtonlessError(error))); mService.terminateConnection(gatt, error | DfuBaseService.ERROR_REMOTE_MASK); } } @@ -161,13 +171,15 @@ import no.nordicsemi.android.error.SecureDfuError; * Checks whether the response received is valid and returns the status code. * * @param response the response received from the DFU device. - * @param request the expected Op Code - * @return the status code - * @throws UnknownResponseException if response was not valid + * @param request the expected Op Code. + * @return The status code. + * @throws UnknownResponseException if response was not valid. */ + @SuppressWarnings("SameParameterValue") private int getStatusCode(final byte[] response, final int request) throws UnknownResponseException { if (response == null || response.length < 3 || response[0] != OP_CODE_RESPONSE_CODE_KEY || response[1] != request || - (response[2] != DFU_STATUS_SUCCESS && response[2] != SecureDfuError.BUTTONLESS_ERROR_OP_CODE_NOT_SUPPORTED && response[2] != SecureDfuError.BUTTONLESS_ERROR_OPERATION_FAILED)) + (response[2] != DFU_STATUS_SUCCESS && response[2] != SecureDfuError.BUTTONLESS_ERROR_OP_CODE_NOT_SUPPORTED + && response[2] != SecureDfuError.BUTTONLESS_ERROR_OPERATION_FAILED)) throw new UnknownResponseException("Invalid response received", response, OP_CODE_RESPONSE_CODE_KEY, request); return response[2]; } diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithBondSharingImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithBondSharingImpl.java index f14e359..3f63d43 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithBondSharingImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithBondSharingImpl.java @@ -29,34 +29,40 @@ import android.content.Intent; import java.util.UUID; +import androidx.annotation.NonNull; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; /** - * This implementation handles the secure buttonless DFU service that will be implemented in SDK 14 or later. - * - * This service requires the device to be paired, so that only a trusted phone can switch it to bootloader mode. - * The bond information will be shared to the bootloader and it will use the same device address when in DFU mode and - * the connection will be encrypted. + * This implementation handles the secure buttonless DFU service that will be implemented in + * SDK 14 or later. + *

+ * This service requires the device to be paired, so that only a trusted phone can switch it to + * bootloader mode. The bond information will be shared to the bootloader and it will use the + * same device address when in DFU mode and the connection will be encrypted. */ /* package */ class ButtonlessDfuWithBondSharingImpl extends ButtonlessDfuImpl { - /** The UUID of the Secure DFU service from SDK 12. */ - protected static final UUID DEFAULT_BUTTONLESS_DFU_SERVICE_UUID = SecureDfuImpl.DEFAULT_DFU_SERVICE_UUID; - /** The UUID of the Secure Buttonless DFU characteristic with bond sharing from SDK 14 or later (not released yet). */ - protected static final UUID DEFAULT_BUTTONLESS_DFU_UUID = new UUID(0x8EC90004F3154F60L, 0x9FB8838830DAEA50L); + /** + * The UUID of the Secure DFU service from SDK 12. + */ + static final UUID DEFAULT_BUTTONLESS_DFU_SERVICE_UUID = SecureDfuImpl.DEFAULT_DFU_SERVICE_UUID; + /** + * The UUID of the Secure Buttonless DFU characteristic with bond sharing from SDK 14 or newer. + */ + static final UUID DEFAULT_BUTTONLESS_DFU_UUID = new UUID(0x8EC90004F3154F60L, 0x9FB8838830DAEA50L); - protected static UUID BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_BUTTONLESS_DFU_SERVICE_UUID; - protected static UUID BUTTONLESS_DFU_UUID = DEFAULT_BUTTONLESS_DFU_UUID; + static UUID BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_BUTTONLESS_DFU_SERVICE_UUID; + static UUID BUTTONLESS_DFU_UUID = DEFAULT_BUTTONLESS_DFU_UUID; private BluetoothGattCharacteristic mButtonlessDfuCharacteristic; - ButtonlessDfuWithBondSharingImpl(final Intent intent, final DfuBaseService service) { + ButtonlessDfuWithBondSharingImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) { super(intent, service); } @Override - public boolean isClientCompatible(final Intent intent, final BluetoothGatt gatt) { + public boolean isClientCompatible(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt) { final BluetoothGattService dfuService = gatt.getService(BUTTONLESS_DFU_SERVICE_UUID); if (dfuService == null) return false; @@ -83,7 +89,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } @Override - public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + public void performDfu(@NonNull final Intent intent) + throws DfuException, DeviceDisconnectedException, UploadAbortedException { logi("Buttonless service with bond sharing found -> SDK 14 or newer"); if (!isBonded()) { logw("Device is not paired, cancelling DFU"); diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithoutBondSharingImpl.java b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithoutBondSharingImpl.java index efdb828..897dddd 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithoutBondSharingImpl.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/ButtonlessDfuWithoutBondSharingImpl.java @@ -29,6 +29,7 @@ import android.content.Intent; import java.util.UUID; +import androidx.annotation.NonNull; import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException; import no.nordicsemi.android.dfu.internal.exception.DfuException; import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; @@ -37,35 +38,38 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; * This implementation handles 2 services: * - a non-secure buttonless DFU service introduced in SDK 13 * - a secure buttonless DFU service that will be implemented in some next SDK (14 or later) - * - * An application that supports one of those services should have the Secure DFU Service with one of those characteristic inside. - * - * The non-secure one does not share the bond information to the bootloader, and the bootloader starts advertising with address +1 after the jump. - * It may be used by a bonded devices (it's even recommended, as it prevents from DoS attack), but the connection with the bootloader will not - * be encrypted (in Secure DFU it is not an issue as the firmware itself is signed). When implemented on a non-bonded device it is - * important to understand, that anyone could connect to the device and switch it to DFU mode preventing the device from normal usage. - * - * The secure one requires the device to be paired so that only the trusted phone can switch it to bootloader mode. - * The bond information will be shared to the bootloader so it will use the same device address when in DFU mode and - * the connection will be encrypted. + *

+ * An application that supports one of those services should have the Secure DFU Service with one + * of those characteristic inside. + *

+ * The non-secure one does not share the bond information to the bootloader, and the bootloader + * starts advertising with address +1 after the jump. It may be used by a bonded devices + * (it's even recommended, as it prevents from DoS attack), but the connection with the bootloader + * will not be encrypted (in Secure DFU it is not an issue as the firmware itself is signed). + * When implemented on a non-bonded device it is important to understand, that anyone could + * connect to the device and switch it to DFU mode preventing the device from normal usage. + *

+ * The secure one requires the device to be paired so that only the trusted phone can switch it + * to bootloader mode. The bond information will be shared to the bootloader so it will use the + * same device address when in DFU mode and the connection will be encrypted. */ /* package */ class ButtonlessDfuWithoutBondSharingImpl extends ButtonlessDfuImpl { /** The UUID of the Secure DFU service from SDK 12. */ - protected static final UUID DEFAULT_BUTTONLESS_DFU_SERVICE_UUID = SecureDfuImpl.DEFAULT_DFU_SERVICE_UUID; + static final UUID DEFAULT_BUTTONLESS_DFU_SERVICE_UUID = SecureDfuImpl.DEFAULT_DFU_SERVICE_UUID; /** The UUID of the Secure Buttonless DFU characteristic without bond sharing from SDK 13. */ - protected static final UUID DEFAULT_BUTTONLESS_DFU_UUID = new UUID(0x8EC90003F3154F60L, 0x9FB8838830DAEA50L); + static final UUID DEFAULT_BUTTONLESS_DFU_UUID = new UUID(0x8EC90003F3154F60L, 0x9FB8838830DAEA50L); - protected static UUID BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_BUTTONLESS_DFU_SERVICE_UUID; - protected static UUID BUTTONLESS_DFU_UUID = DEFAULT_BUTTONLESS_DFU_UUID; + static UUID BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_BUTTONLESS_DFU_SERVICE_UUID; + static UUID BUTTONLESS_DFU_UUID = DEFAULT_BUTTONLESS_DFU_UUID; private BluetoothGattCharacteristic mButtonlessDfuCharacteristic; - ButtonlessDfuWithoutBondSharingImpl(final Intent intent, final DfuBaseService service) { + ButtonlessDfuWithoutBondSharingImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) { super(intent, service); } @Override - public boolean isClientCompatible(final Intent intent, final BluetoothGatt gatt) { + public boolean isClientCompatible(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt) { final BluetoothGattService dfuService = gatt.getService(BUTTONLESS_DFU_SERVICE_UUID); if (dfuService == null) return false; @@ -92,7 +96,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException; } @Override - public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException { + public void performDfu(@NonNull final Intent intent) + throws DfuException, DeviceDisconnectedException, UploadAbortedException { logi("Buttonless service without bond sharing found -> SDK 13 or newer"); if (isBonded()) { logw("Device is paired! Use Buttonless DFU with Bond Sharing instead (SDK 14 or newer)"); diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java index 824cd61..6abe3ea 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java @@ -46,6 +46,9 @@ import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.MediaStore; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; @@ -90,6 +93,7 @@ import no.nordicsemi.android.error.GattError; * The service will show its progress on the notification bar and will send local broadcasts to the * application. */ +@SuppressWarnings("deprecation") public abstract class DfuBaseService extends IntentService implements DfuProgressInfo.ProgressListener { private static final String TAG = "DfuBaseService"; @@ -107,60 +111,96 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.dfu.extra.EXTRA_DEVICE_NAME"; /** - * A boolean indicating whether to disable the progress notification in the status bar. Defaults to false. + * A boolean indicating whether to disable the progress notification in the status bar. + * Defaults to false. */ public static final String EXTRA_DISABLE_NOTIFICATION = "no.nordicsemi.android.dfu.extra.EXTRA_DISABLE_NOTIFICATION"; /** - * A boolean indicating whether the DFU service should be set as a foreground service. It is recommended to have it - * as a background service at least on Android Oreo or newer as the background service will be killed by the system - * few moments after the user closed the foreground app. - *

Read more here: https://developer.android.com/about/versions/oreo/background.html

+ * A boolean indicating whether the DFU service should be set as a foreground service. + * It is recommended to have it as a background service at least on Android Oreo or newer as + * the background service will be killed by the system few moments after the user closed the + * foreground app. + *

+ * Read more here: https://developer.android.com/about/versions/oreo/background.html */ public static final String EXTRA_FOREGROUND_SERVICE = "no.nordicsemi.android.dfu.extra.EXTRA_FOREGROUND_SERVICE"; - /** An extra private field indicating which attempt is being performed. In case of error 133 the service will retry to connect one more time. */ - private static final String EXTRA_ATTEMPT = "no.nordicsemi.android.dfu.extra.EXTRA_ATTEMPT"; /** + * An extra private field indicating which reconnection attempt is being performed. + * In case of error 133 the service will retry to connect 2 more times. + */ + private static final String EXTRA_RECONNECTION_ATTEMPT = "no.nordicsemi.android.dfu.extra.EXTRA_RECONNECTION_ATTEMPT"; + /** + * An extra private field indicating which DFU attempt is being performed. + * If the target device will disconnect for some unknown reason during DFU, the service will + * retry to connect and continue. In case of Legacy DFU it will reconnect and restart process. + */ + /* package */ static final String EXTRA_DFU_ATTEMPT = "no.nordicsemi.android.dfu.extra.EXTRA_DFU_ATTEMPT"; + /** + * Maximum number of DFU attempts. Default value is 0. + */ + public static final String EXTRA_MAX_DFU_ATTEMPTS = "no.nordicsemi.android.dfu.extra.EXTRA_MAX_DFU_ATTEMPTS"; + /** + * If the new firmware (application) does not share the bond information with the old one, + * the bond information is lost. Set this flag to 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. Note: currently it is not possible to remove the old bond without 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. *

- * If the new firmware (application) does not share the bond information with the old one, the bond information is lost. Set this flag to 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. Note: currently it is not possible to remove the old bond without 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. - *

+ * In case of updating the soft device the application is always removed together with the + * bond information. *

- * In case of updating the soft device the application is always removed together with the bond information. - *

+ * Search for occurrences of EXTRA_RESTORE_BOND in this file to check the implementation and + * get more details. *

- * Search for occurrences of EXTRA_RESTORE_BOND in this file to check the implementation and get more details. - *

- *

This flag is ignored when Secure DFU Buttonless Service is used. It will keep or will not restore the bond depending on the Buttonless service type.

+ * This flag is ignored when Secure DFU Buttonless Service is used. + * It will keep or will not restore the bond depending on the Buttonless service type. */ public static final String EXTRA_RESTORE_BOND = "no.nordicsemi.android.dfu.extra.EXTRA_RESTORE_BOND"; /** - *

This flag indicated whether the bond information should be kept or removed after an upgrade of the Application. - * If an application is being updated on a bonded device with the DFU Bootloader that has been configured to preserve the bond information for the new application, - * set it to true.

- * - *

By default the Legacy DFU Bootloader clears the whole application's memory. It may be however configured in the \Nordic\nrf51\components\libraries\bootloader_dfu\dfu_types.h - * file (sdk 11, line 76: #define DFU_APP_DATA_RESERVED 0x0000) to preserve some pages. The BLE_APP_HRM_DFU sample app stores the LTK and System Attributes in the first - * two pages, so in order to preserve the bond information this value should be changed to 0x0800 or more. For Secure DFU this value is by default set to 3 pages. - * When those data are preserved, the new Application will notify the app with the Service Changed indication when launched for the first time. Otherwise this - * service will remove the bond information from the phone and force to refresh the device cache (see {@link #refreshDeviceCache(android.bluetooth.BluetoothGatt, boolean)}).

- * - *

In contrast to {@link #EXTRA_RESTORE_BOND} this flag will not remove the old bonding and recreate a new one, but will keep the bond information untouched.

- *

The default value of this flag is false.

- * - *

This flag is ignored when Secure DFU Buttonless Service is used. It will keep or remove the bond depending on the Buttonless service type.

+ * This flag indicated whether the bond information should be kept or removed after an upgrade + * of the Application. If an application is being updated on a bonded device with the DFU + * Bootloader that has been configured to preserve the bond information for the new application, + * set it to true. + *

+ * By default the Legacy DFU Bootloader clears the whole application's memory. It may be, + * however, configured in the \Nordic\nrf51\components\libraries\bootloader_dfu\dfu_types.h + * file (sdk 11, line 76: #define DFU_APP_DATA_RESERVED 0x0000) to preserve some pages. + * The BLE_APP_HRM_DFU sample app stores the LTK and System Attributes in the first + * two pages, so in order to preserve the bond information this value should be changed to + * 0x0800 or more. For Secure DFU this value is by default set to 3 pages. + * When those data are preserved, the new Application will notify the app with the + * Service Changed indication when launched for the first time. Otherwise this service will + * remove the bond information from the phone and force to refresh the device cache + * (see {@link #refreshDeviceCache(android.bluetooth.BluetoothGatt, boolean)}). + *

+ * In contrast to {@link #EXTRA_RESTORE_BOND} this flag will not remove the old bonding and + * recreate a new one, but will keep the bond information untouched. + *

+ * The default value of this flag is false. + *

+ * This flag is ignored when Secure DFU Buttonless Service is used. It will keep or remove the + * bond depending on the Buttonless service type. */ public static final String EXTRA_KEEP_BOND = "no.nordicsemi.android.dfu.extra.EXTRA_KEEP_BOND"; /** * This property must contain a boolean value. - *

The {@link DfuBaseService}, when connected to a DFU target will check whether it is in application or in DFU bootloader mode. For DFU implementations from SDK 7.0 or newer - * this is done by reading the value of DFU Version characteristic. If the returned value is equal to 0x0100 (major = 0, minor = 1) it means that we are in the application mode and - * jump to the bootloader mode is required. - *

However, for DFU implementations from older SDKs, where there was no DFU Version characteristic, the service must guess. If this option is set to false (default) it will count - * number of device's services. If the count is equal to 3 (Generic Access, Generic Attribute, DFU Service) it will assume that it's in DFU mode. If greater than 3 - in app mode. - * This guessing may not be always correct. One situation may be when the nRF chip is used to flash update on external MCU using DFU. The DFU procedure may be implemented in the - * application, which may (and usually does) have more services. In such case set the value of this property to true. + *

+ * The {@link DfuBaseService}, when connected to a DFU target will check whether it is in + * application or in DFU bootloader mode. For DFU implementations from SDK 7.0 or newer + * this is done by reading the value of DFU Version characteristic. + * If the returned value is equal to 0x0100 (major = 0, minor = 1) it means that we are in the + * application mode and jump to the bootloader mode is required. + *

+ * However, for DFU implementations from older SDKs, where there was no DFU Version + * characteristic, the service must guess. If this option is set to false (default) it will count + * number of device's services. If the count is equal to 3 (Generic Access, Generic Attribute, + * DFU Service) it will assume that it's in DFU mode. If greater than 3 - in app mode. + * This guessing may not be always correct. One situation may be when the nRF chip is used to + * flash update on external MCU using DFU. The DFU procedure may be implemented in the + * application, which may (and usually does) have more services. + * In such case set the value of this property to true. */ public static final String EXTRA_FORCE_DFU = "no.nordicsemi.android.dfu.extra.EXTRA_FORCE_DFU"; /** @@ -175,6 +215,12 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * #71. */ public static final String EXTRA_DISABLE_RESUME = "no.nordicsemi.android.dfu.extra.EXTRA_DISABLE_RESUME"; + /** + * The MBR size. + * + * @see DfuServiceInitiator#setMbrSize(int) + */ + public static final String EXTRA_MBR_SIZE = "no.nordicsemi.android.dfu.extra.EXTRA_MBR_SIZE"; /** * This extra allows you to control the MTU that will be requested (on Lollipop or newer devices). * If the field is null, the service will not request higher MTU and will use MTU = 23 @@ -195,13 +241,14 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres *

* In the SDK 12.x the Buttonless DFU feature for Secure DFU was experimental. * It is NOT recommended to use it: it was not properly tested, had implementation bugs - * (e.g. https://devzone.nordicsemi.com/question/100609/sdk-12-bootloader-erased-after-programming/) and - * does not required encryption and therefore may lead to DOS attack (anyone can use it to switch the device - * to bootloader mode). However, as there is no other way to trigger bootloader mode on devices - * without a button, this DFU Library supports this service, but the feature must be explicitly enabled here. - * Be aware, that setting this flag to false will no protect your devices from this kind of attacks, as - * an attacker may use another app for that purpose. To be sure your device is secure remove this - * experimental service from your device. + * (e.g. https://devzone.nordicsemi.com/question/100609/sdk-12-bootloader-erased-after-programming/) + * and does not required encryption and therefore may lead to DOS attack (anyone can use it + * to switch the device to bootloader mode). However, as there is no other way to trigger + * bootloader mode on devices without a button, this DFU Library supports this service, + * but the feature must be explicitly enabled here. + * Be aware, that setting this flag to false will no protect your devices from this kind of + * attacks, as an attacker may use another app for that purpose. To be sure your device is + * secure remove this experimental service from your device. *

* Spec:
* Buttonless DFU Service UUID: 8E400001-F315-4F60-9FB8-838830DAEA50
@@ -214,36 +261,53 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * The device should disconnect and restart in DFU mode after sending the notification. *

* In SDK 13 this issue will be fixed by a proper implementation (bonding required, - * passing bond information to the bootloader, encryption, well tested). It is recommended to use this - * new service when SDK 13 (or later) is out. TODO: fix the docs when SDK 13 is out. + * passing bond information to the bootloader, encryption, well tested). + * It is recommended to use this new service when SDK 13 (or later) is out. */ public static final String EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU = "no.nordicsemi.android.dfu.extra.EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU"; /** * This property must contain a boolean value. - *

If true the Packet Receipt Notification procedure will be enabled. See DFU documentation on http://infocenter.nordicsemi.com for more details. - * The number of packets before receiving a Packet Receipt Notification is set with property {@link #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE}. - * The PRNs by default are enabled on devices running Android 4.3, 4.4.x and 5.x and disabled on 6.x and newer. + *

+ * If true the Packet Receipt Notification procedure will be enabled. + * See DFU documentation on http://infocenter.nordicsemi.com for more details. + * The number of packets before receiving a Packet Receipt Notification is set with property + * {@link #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE}. + * The PRNs by default are enabled on devices running Android 4.3, 4.4.x and 5.x and + * disabled on 6.x and newer. + * * @see #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE */ public static final String EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED = "no.nordicsemi.android.dfu.extra.EXTRA_PRN_ENABLED"; /** * This property must contain a positive integer value, usually from range 1-200. - *

The default value is {@link DfuServiceInitiator#DEFAULT_PRN_VALUE}. Setting it to 0 will disable the Packet Receipt Notification procedure. - * When sending a firmware using the DFU procedure the service will send this number of packets before waiting for a notification. - * Packet Receipt Notifications are used to synchronize the sender with receiver. - *

On Android, calling {@link android.bluetooth.BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic)} - * simply adds the packet to outgoing queue before returning the callback. Adding the next packet in the callback is much faster than the real transmission - * (also the speed depends on the device chip manufacturer) and the queue may reach its limit. When does, the transmission stops and Android Bluetooth hangs (see Note below). - * Using PRN procedure eliminates this problem as the notification is send when all packets were delivered the queue is empty. - *

Note: this bug has been fixed on Android 6.0 Marshmallow and now no notifications are required. The onCharacteristicWrite callback will be - * postponed until half of the queue is empty and upload will be resumed automatically. Disabling PRNs speeds up the upload process on those devices. + *

+ * The default value is {@link DfuServiceInitiator#DEFAULT_PRN_VALUE}. + * Setting it to 0 will disable the Packet Receipt Notification procedure. + * When sending a firmware using the DFU procedure the service will send this number of packets + * before waiting for a notification. Packet Receipt Notifications are used to synchronize + * the sender with receiver. + *

+ * On Android, calling + * {@link android.bluetooth.BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic)} + * simply adds the packet to outgoing queue before returning the callback. Adding the next + * packet in the callback is much faster than the real transmission (also the speed depends on + * the device chip manufacturer) and the queue may reach its limit. When does, the transmission + * stops and Android Bluetooth hangs (see Note below). Using PRN procedure eliminates this + * problem as the notification is send when all packets were delivered the queue is empty. + *

+ * Note: this bug has been fixed on Android 6.0 Marshmallow and now no notifications are required. + * The onCharacteristicWrite callback will be postponed until half of the queue is empty and + * upload will be resumed automatically. Disabling PRNs speeds up the upload process on those + * devices. + * * @see #EXTRA_PACKET_RECEIPT_NOTIFICATIONS_ENABLED */ public static final String EXTRA_PACKET_RECEIPT_NOTIFICATIONS_VALUE = "no.nordicsemi.android.dfu.extra.EXTRA_PRN_VALUE"; /** * A path to the file with the new firmware. It may point to a HEX, BIN or a ZIP file. - * Some file manager applications return the path as a String while other return a Uri. Use the {@link #EXTRA_FILE_URI} in the later case. - * For files included in /res/raw resource directory please use {@link #EXTRA_FILE_RES_ID} instead. + * Some file manager applications return the path as a String while other return a Uri. + * Use the {@link #EXTRA_FILE_URI} in the later case. For files included + * in /res/raw resource directory please use {@link #EXTRA_FILE_RES_ID} instead. */ public static final String EXTRA_FILE_PATH = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_PATH"; /** @@ -255,26 +319,33 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final String EXTRA_FILE_RES_ID = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_RES_ID"; /** - * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). Must point to a 'dat' file corresponding with the selected firmware. - * The Init packet may contain just the CRC (in case of older versions of DFU) or the Extended Init Packet in binary format (SDK 7.0+). + * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). + * Must point to a 'dat' file corresponding with the selected firmware. + * The Init packet may contain just the CRC (in case of older versions of DFU) or the + * Extended Init Packet in binary format (SDK 7.0+). */ public static final String EXTRA_INIT_FILE_PATH = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_PATH"; /** - * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). Must point to a 'dat' file corresponding with the selected firmware. - * The Init packet may contain just the CRC (in case of older versions of DFU) or the Extended Init Packet in binary format (SDK 7.0+). + * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). + * Must point to a 'dat' file corresponding with the selected firmware. + * The Init packet may contain just the CRC (in case of older versions of DFU) or the + * Extended Init Packet in binary format (SDK 7.0+). */ public static final String EXTRA_INIT_FILE_URI = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_URI"; /** - * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). Must point to a 'dat' file corresponding with the selected firmware. - * The Init packet may contain just the CRC (in case of older versions of DFU) or the Extended Init Packet in binary format (SDK 7.0+). + * The Init packet URI. This file is required if the Extended Init Packet is required (SDK 7.0+). + * Must point to a 'dat' file corresponding with the selected firmware. + * The Init packet may contain just the CRC (in case of older versions of DFU) or the + * Extended Init Packet in binary format (SDK 7.0+). */ public static final String EXTRA_INIT_FILE_RES_ID = "no.nordicsemi.android.dfu.extra.EXTRA_INIT_FILE_RES_ID"; /** - * The input file mime-type. Currently only "application/zip" (ZIP) or "application/octet-stream" (HEX or BIN) are supported. If this parameter is - * empty the "application/octet-stream" is assumed. + * The input file mime-type. Currently only "application/zip" (ZIP) or "application/octet-stream" + * (HEX or BIN) are supported. If this parameter is empty the "application/octet-stream" is assumed. */ public static final String EXTRA_FILE_MIME_TYPE = "no.nordicsemi.android.dfu.extra.EXTRA_MIME_TYPE"; - // Since the DFU Library version 0.5 both HEX and BIN files are supported. As both files have the same MIME TYPE the distinction is made based on the file extension. + // Since the DFU Library version 0.5 both HEX and BIN files are supported. + // As both files have the same MIME TYPE the distinction is made based on the file extension. public static final String MIME_TYPE_OCTET_STREAM = "application/octet-stream"; public static final String MIME_TYPE_ZIP = "application/zip"; /** @@ -283,27 +354,32 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres *

  • {@link #TYPE_SOFT_DEVICE} - only Soft Device update
  • *
  • {@link #TYPE_BOOTLOADER} - only Bootloader update
  • *
  • {@link #TYPE_APPLICATION} - only application update
  • - *
  • {@link #TYPE_AUTO} - the file is a ZIP file that may contain more than one HEX/BIN + DAT files. Since SDK 8.0 the ZIP Distribution packet is a recommended - * way of delivering firmware files. Please, see the DFU documentation for more details. A ZIP distribution packet may be created using the 'nrf utility' - * command line application, that is a part of Master Control Panel 3.8.0.The ZIP file MAY contain only the following files: - * softdevice.hex/bin, bootloader.hex/bin, application.hex/bin to determine the type based on its name. At lease one of them MUST be present. + *
  • {@link #TYPE_AUTO} - the file is a ZIP file that may contain more than one HEX/BIN + DAT files. + * Since SDK 8.0 the ZIP Distribution packet is a recommended way of delivering firmware files. + * Please, see the DFU documentation for more details. A ZIP distribution packet may be created + * using the 'nrf util' Python application, available at + * https://github.com/NordicSemiconductor/pc-nrfutil. + * The ZIP file MAY contain only the following files: softdevice.hex/bin, + * bootloader.hex/bin, application.hex/bin to determine the type based on its name. + * At lease one of them MUST be present. *
  • * * If this parameter is not provided the type is assumed as follows: *
      - *
    1. If the {@link #EXTRA_FILE_MIME_TYPE} field is null or is equal to {@value #MIME_TYPE_OCTET_STREAM} - the {@link #TYPE_APPLICATION} is assumed.
    2. - *
    3. If the {@link #EXTRA_FILE_MIME_TYPE} field is equal to {@value #MIME_TYPE_ZIP} - the {@link #TYPE_AUTO} is assumed.
    4. + *
    5. If the {@link #EXTRA_FILE_MIME_TYPE} field is null or is equal to + * {@value #MIME_TYPE_OCTET_STREAM} - the {@link #TYPE_APPLICATION} is assumed.
    6. + *
    7. If the {@link #EXTRA_FILE_MIME_TYPE} field is equal to {@value #MIME_TYPE_ZIP} + * - the {@link #TYPE_AUTO} is assumed.
    8. *
    */ public static final String EXTRA_FILE_TYPE = "no.nordicsemi.android.dfu.extra.EXTRA_FILE_TYPE"; /** *

    * The file contains a new version of Soft Device. - *

    *

    - * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required if Extended Init Packet is used by the DFU bootloader (SDK 7.0+).. + * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required + * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+).. * The Init packet for the bootloader must be placed in the .dat file. - *

    * * @see #EXTRA_FILE_TYPE */ @@ -311,11 +387,10 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** *

    * The file contains a new version of Bootloader. - *

    *

    - * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required if Extended Init Packet is used by the DFU bootloader (SDK 7.0+). + * Since DFU Library 7.0 all firmware may contain an Init packet. The Init packet is required + * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+). * The Init packet for the bootloader must be placed in the .dat file. - *

    * * @see #EXTRA_FILE_TYPE */ @@ -323,31 +398,37 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** *

    * The file contains a new version of Application. - *

    *

    - * Since DFU Library 0.5 all firmware may contain an Init packet. The Init packet is required if Extended Init Packet is used by the DFU bootloader (SDK 7.0+). + * Since DFU Library 0.5 all firmware may contain an Init packet. The Init packet is required + * if Extended Init Packet is used by the DFU bootloader (SDK 7.0+). * The Init packet for the application must be placed in the .dat file. - *

    * * @see #EXTRA_FILE_TYPE */ public static final int TYPE_APPLICATION = 0x04; /** *

    - * A ZIP file that consists of more than 1 file. Since SDK 8.0 the ZIP Distribution packet is a recommended way of delivering firmware files. Please, see the DFU documentation for - * more details. A ZIP distribution packet may be created using the 'nrf utility' command line application, that is a part of Master Control Panel 3.8.0. - * For backwards compatibility this library supports also ZIP files without the manifest file. Instead they must follow the fixed naming convention: - * The names of files in the ZIP must be: softdevice.hex (or .bin), bootloader.hex (or .bin), application.hex (or .bin) in order - * to be read correctly. Using the Soft Device v7.0.0+ the Soft Device and Bootloader may be updated and sent together. In case of additional application file included, - * the service will try to send Soft Device, Bootloader and Application together (which is not supported currently) and if it fails, send first SD+BL, reconnect and send the application - * in the following connection. - *

    + * A ZIP file that consists of more than 1 file. Since SDK 8.0 the ZIP Distribution packet is + * a recommended way of delivering firmware files. Please, see the DFU documentation for + * more details. A ZIP distribution packet may be created using the 'nrf utility' command line + * application, that is a part of Master Control Panel 3.8.0. + * For backwards compatibility this library supports also ZIP files without the manifest file. + * Instead they must follow the fixed naming convention: + * The names of files in the ZIP must be: softdevice.hex (or .bin), bootloader.hex + * (or .bin), application.hex (or .bin) in order to be read correctly. Using the + * Soft Device v7.0.0+ the Soft Device and Bootloader may be updated and sent together. + * In case of additional application file included, the service will try to send Soft Device, + * Bootloader and Application together (which is not supported currently) and if it fails, + * send first SD+BL, reconnect and send the application in the following connection. *

    - * Since the DFU Library 0.5 you may specify the Init packet, that will be send prior to the firmware. The init packet contains some verification data, like a device type and - * revision, application version or a list of supported Soft Devices. The Init packet is required if Extended Init Packet is used by the DFU bootloader (SDK 7.0+). - * In case of using the compatibility ZIP files the Init packet for the Soft Device and Bootloader must be in the 'system.dat' file while for the application - * in the 'application.dat' file (included in the ZIP). The CRC in the 'system.dat' must be a CRC of both BIN contents if both a Soft Device and a Bootloader is present. - *

    + * Since the DFU Library 0.5 you may specify the Init packet, that will be send prior to the + * firmware. The init packet contains some verification data, like a device type and revision, + * application version or a list of supported Soft Devices. The Init packet is required if + * Extended Init Packet is used by the DFU bootloader (SDK 7.0+). + * In case of using the compatibility ZIP files the Init packet for the Soft Device and Bootloader + * must be in the 'system.dat' file while for the application in the 'application.dat' file + * (included in the ZIP). The CRC in the 'system.dat' must be a CRC of both BIN contents if + * both a Soft Device and a Bootloader is present. * * @see #EXTRA_FILE_TYPE */ @@ -357,7 +438,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final String EXTRA_DATA = "no.nordicsemi.android.dfu.extra.EXTRA_DATA"; /** - * An extra field to send the progress or error information in the DFU notification. The value may contain: + * An extra field to send the progress or error information in the DFU notification. + * The value may contain: * * To check if error occurred use:
    * {@code boolean error = progressValue >= DfuBaseService.ERROR_MASK;} */ public static final String EXTRA_PROGRESS = "no.nordicsemi.android.dfu.extra.EXTRA_PROGRESS"; /** - * The number of currently transferred part. The SoftDevice and Bootloader may be send together as one part. If user wants to upload them together with an application it has to be sent - * in another connection as the second part. + * The number of currently transferred part. The SoftDevice and Bootloader may be send + * together as one part. If user wants to upload them together with an application it has to be + * sent in another connection as the second part. * * @see no.nordicsemi.android.dfu.DfuBaseService#EXTRA_PARTS_TOTAL */ @@ -416,11 +500,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * *
  • {@link #EXTRA_DEVICE_ADDRESS} - the target device address
  • *
  • {@link #EXTRA_PART_CURRENT} - the number of currently transmitted part
  • - *
  • {@link #EXTRA_PARTS_TOTAL} - total number of parts that are being sent, f.e. if a ZIP file contains a Soft Device, a Bootloader and an Application, - * the SoftDevice and Bootloader will be send together as one part. Then the service will disconnect and reconnect to the new Bootloader and send the - * application as part number two.
  • + *
  • {@link #EXTRA_PARTS_TOTAL} - total number of parts that are being sent, e.g. if a ZIP + * file contains a Soft Device, a Bootloader and an Application, the SoftDevice and Bootloader + * will be send together as one part. Then the service will disconnect and reconnect to the + * new Bootloader and send the application as part number two.
  • *
  • {@link #EXTRA_SPEED_B_PER_MS} - current speed in bytes/millisecond as float
  • - *
  • {@link #EXTRA_AVG_SPEED_B_PER_MS} - the average transmission speed in bytes/millisecond as float
  • + *
  • {@link #EXTRA_AVG_SPEED_B_PER_MS} - the average transmission speed in bytes/millisecond + * as float
  • * */ public static final String BROADCAST_PROGRESS = "no.nordicsemi.android.dfu.broadcast.BROADCAST_PROGRESS"; @@ -433,8 +519,9 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final int PROGRESS_STARTING = -2; /** - * Service has triggered a switch to bootloader mode. Now the service waits for the link loss event (this may take up to several seconds) and will connect again - * to the same device, now started in the bootloader mode. + * Service has triggered a switch to bootloader mode. Now the service waits for the link loss + * event (this may take up to several seconds) and will connect again to the same device, + * now started in the bootloader mode. */ public static final int PROGRESS_ENABLING_DFU_MODE = -3; /** @@ -456,15 +543,19 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** * The broadcast error message contains the following extras: * */ public static final String BROADCAST_ERROR = "no.nordicsemi.android.dfu.broadcast.BROADCAST_ERROR"; /** - * The type of the error. This extra contains information about that kind of error has occurred. Connection state errors and other errors may share the same numbers. - * For example, the {@link BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)} method may return a status code 8 (GATT INSUF AUTHORIZATION), - * while the status code 8 returned by {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} is a GATT CONN TIMEOUT error. + * The type of the error. This extra contains information about that kind of error has occurred. + * Connection state errors and other errors may share the same numbers. For example, the + * {@link BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)} + * method may return a status code 8 (GATT INSUF AUTHORIZATION), while the status code 8 + * returned by {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} + * is a GATT CONN TIMEOUT error. */ public static final String EXTRA_ERROR_TYPE = "no.nordicsemi.android.dfu.extra.EXTRA_ERROR_TYPE"; public static final int ERROR_TYPE_OTHER = 0; @@ -472,7 +563,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres public static final int ERROR_TYPE_COMMUNICATION = 2; public static final int ERROR_TYPE_DFU_REMOTE = 3; /** - * If this bit is set than the progress value indicates an error. Use {@link GattError#parse(int)} to obtain error name. + * If this bit is set than the progress value indicates an error. Use {@link GattError#parse(int)} + * to obtain error name. */ public static final int ERROR_MASK = 0x1000; public static final int ERROR_DEVICE_DISCONNECTED = ERROR_MASK; // | 0x00; @@ -494,11 +586,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final int ERROR_SERVICE_DISCOVERY_NOT_STARTED = ERROR_MASK | 0x05; /** - * Thrown when the service discovery has finished but the DFU service has not been found. The device does not support DFU of is not in DFU mode. + * Thrown when the service discovery has finished but the DFU service has not been found. + * The device does not support DFU of is not in DFU mode. */ public static final int ERROR_SERVICE_NOT_FOUND = ERROR_MASK | 0x06; /** - * Thrown when unknown response has been obtained from the target. The DFU target must follow specification. + * Thrown when unknown response has been obtained from the target. The DFU target must follow + * specification. */ public static final int ERROR_INVALID_RESPONSE = ERROR_MASK | 0x08; /** @@ -510,15 +604,18 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final int ERROR_BLUETOOTH_DISABLED = ERROR_MASK | 0x0A; /** - * DFU Bootloader version 0.6+ requires sending the Init packet. If such bootloader version is detected, but the init packet has not been set this error is thrown. + * DFU Bootloader version 0.6+ requires sending the Init packet. If such bootloader version is + * detected, but the init packet has not been set this error is thrown. */ public static final int ERROR_INIT_PACKET_REQUIRED = ERROR_MASK | 0x0B; - /** - * Thrown when the firmware file is not word-aligned. The firmware size must be dividable by 4 bytes. - */ - public static final int ERROR_FILE_SIZE_INVALID = ERROR_MASK | 0x0C; /** - * Thrown when the received CRC does not match with the calculated one. The service will try 3 times to send the data, and if the CRC fails each time this error will be thrown. + * Thrown when the firmware file is not word-aligned. The firmware size must be dividable by + * 4 bytes. + */ + public static final int ERROR_FILE_SIZE_INVALID = ERROR_MASK | 0x0C; + /** + * Thrown when the received CRC does not match with the calculated one. The service will try + * 3 times to send the data, and if the CRC fails each time this error will be thrown. */ public static final int ERROR_CRC_ERROR = ERROR_MASK | 0x0D; /** @@ -526,8 +623,9 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final int ERROR_DEVICE_NOT_BONDED = ERROR_MASK | 0x0E; /** - * Flag set when the DFU target returned a DFU error. Look for DFU specification to get error codes. The error code is binary OR-ed with one of: - * {@link #ERROR_REMOTE_TYPE_LEGACY}, {@link #ERROR_REMOTE_TYPE_SECURE} or {@link #ERROR_REMOTE_TYPE_SECURE_EXTENDED}. + * Flag set when the DFU target returned a DFU error. Look for DFU specification to get error + * codes. The error code is binary OR-ed with one of: {@link #ERROR_REMOTE_TYPE_LEGACY}, + * {@link #ERROR_REMOTE_TYPE_SECURE} or {@link #ERROR_REMOTE_TYPE_SECURE_EXTENDED}. */ public static final int ERROR_REMOTE_MASK = 0x2000; public static final int ERROR_REMOTE_TYPE_LEGACY = 0x0100; @@ -535,19 +633,23 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres public static final int ERROR_REMOTE_TYPE_SECURE_EXTENDED = 0x0400; public static final int ERROR_REMOTE_TYPE_SECURE_BUTTONLESS = 0x0800; /** - * The flag set when one of {@link android.bluetooth.BluetoothGattCallback} methods was called with status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * The flag set when one of {@link android.bluetooth.BluetoothGattCallback} methods was called + * with status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. */ public static final int ERROR_CONNECTION_MASK = 0x4000; /** - * The flag set when the {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} method was called with - * status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * The flag set when the + * {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} + * method was called with status other than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. */ public static final int ERROR_CONNECTION_STATE_MASK = 0x8000; /** - * The log events are only broadcast when there is no nRF Logger installed. The broadcast contains 2 extras: + * The log events are only broadcast when there is no nRF Logger installed. + * The broadcast contains 2 extras: * */ @@ -557,8 +659,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /* * Note: * The nRF Logger API library has been excluded from the DfuLibrary. - * All log events are now being sent using local broadcasts and may be logged into nRF Logger in the app module. - * This is to make the Dfu module independent from logging tool. + * All log events are now being sent using local broadcasts and may be logged into nRF Logger + * in the app module. This is to make the Dfu module independent from logging tool. * * The log levels below are equal to log levels in nRF Logger API library, v 2.0. * @see https://github.com/NordicSemiconductor/nRF-Logger-API @@ -593,18 +695,24 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ public static final String BROADCAST_ACTION = "no.nordicsemi.android.dfu.broadcast.BROADCAST_ACTION"; /** - * The action extra. It may have one of the following values: {@link #ACTION_PAUSE}, {@link #ACTION_RESUME}, {@link #ACTION_ABORT}. + * The action extra. It may have one of the following values: {@link #ACTION_PAUSE}, + * {@link #ACTION_RESUME}, {@link #ACTION_ABORT}. */ public static final String EXTRA_ACTION = "no.nordicsemi.android.dfu.extra.EXTRA_ACTION"; - /** Pauses the upload. The service will wait for broadcasts with the action set to {@link #ACTION_RESUME} or {@link #ACTION_ABORT}. */ + /** + * Pauses the upload. The service will wait for broadcasts with the action set to + * {@link #ACTION_RESUME} or {@link #ACTION_ABORT}. + */ public static final int ACTION_PAUSE = 0; /** Resumes the upload that has been paused before using {@link #ACTION_PAUSE}. */ public static final int ACTION_RESUME = 1; /** * Aborts the upload. The service does not need to be paused before. - * After sending {@link #BROADCAST_ACTION} with extra {@link #EXTRA_ACTION} set to this value the DFU bootloader will restore the old application - * (if there was already an application). Be aware that uploading the Soft Device will erase the application in order to make space in the memory. - * In case there is no application, or the application has been removed, the DFU bootloader will be started and user may try to send the application again. + * After sending {@link #BROADCAST_ACTION} with extra {@link #EXTRA_ACTION} set to this value + * the DFU bootloader will restore the old application (if there was already an application). + * Be aware, that uploading the Soft Device will erase the application in order to make space + * in the memory. In case there is no application, or the application has been removed, the + * DFU bootloader will be started and user may try to send the application again. * The bootloader may advertise with the address incremented by 1 to prevent caching services. */ public static final int ACTION_ABORT = 2; @@ -624,7 +732,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres private String mDeviceName; private boolean mDisableNotification; /** - * The current connection state. If its value is > 0 than an error has occurred. Error number is a negative value of mConnectionState + * The current connection state. If its value is > 0 than an error has occurred. + * Error number is a negative value of mConnectionState */ protected int mConnectionState; protected final static int STATE_DISCONNECTED = 0; @@ -638,7 +747,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres */ private int mError; /** - * Stores the last progress percent. Used to prevent from sending progress notifications with the same value. + * Stores the last progress percent. Used to prevent from sending progress notifications with + * the same value. */ private int mLastProgress = -1; /* package */ DfuProgressInfo mProgressInfo; @@ -1000,17 +1110,25 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres mConnectionState = STATE_DISCONNECTED; mError = 0; - // The Soft Device starts where MBR ends (by default from the address 0x1000). Before there is a MBR section, which should not be transmitted over DFU. - // Applications and bootloader starts from bigger address. However, in custom DFU implementations, user may want to transmit the whole whole data, even from address 0x0000. + // The Soft Device starts where MBR ends (by default from the address 0x1000). + // Before there is a MBR section, which should not be transmitted over DFU. + // Applications and bootloader starts from bigger address. However, in custom DFU + // implementations, user may want to transmit the whole whole data, even from address 0x0000. final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final String value = preferences.getString(DfuSettingsConstants.SETTINGS_MBR_SIZE, String.valueOf(DfuSettingsConstants.SETTINGS_DEFAULT_MBR_SIZE)); - int mbrSize; - try { - mbrSize = Integer.parseInt(value); + int mbrSize = DfuServiceInitiator.DEFAULT_MBR_SIZE; + if (preferences.contains(DfuSettingsConstants.SETTINGS_MBR_SIZE)) { + final String value = preferences.getString(DfuSettingsConstants.SETTINGS_MBR_SIZE, String.valueOf(DfuServiceInitiator.DEFAULT_MBR_SIZE)); + try { + mbrSize = Integer.parseInt(value); + if (mbrSize < 0) + mbrSize = 0; + } catch (final NumberFormatException e) { + // ignore, default value will be used + } + } else { + mbrSize = intent.getIntExtra(EXTRA_MBR_SIZE, DfuServiceInitiator.DEFAULT_MBR_SIZE); if (mbrSize < 0) mbrSize = 0; - } catch (final NumberFormatException e) { - mbrSize = DfuSettingsConstants.SETTINGS_DEFAULT_MBR_SIZE; } if (foregroundService) { @@ -1152,7 +1270,9 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres sendLogBroadcast(LOG_LEVEL_VERBOSE, "Connecting to DFU target..."); mProgressInfo.setProgress(PROGRESS_CONNECTING); + final long before = SystemClock.elapsedRealtime(); final BluetoothGatt gatt = connect(deviceAddress); + final long after = SystemClock.elapsedRealtime(); // Are we connected? if (gatt == null) { loge("Bluetooth adapter disabled"); @@ -1160,30 +1280,26 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres report(ERROR_BLUETOOTH_DISABLED); return; } - if (mConnectionState == STATE_DISCONNECTED) { - if (mError == (ERROR_CONNECTION_STATE_MASK | 133)) { - loge("Device not reachable. Check if the device with address " + deviceAddress + " is in range, is advertising and is connectable"); - sendLogBroadcast(LOG_LEVEL_ERROR, "Error 133: Connection timeout"); - } else { - loge("Device got disconnected before service discovery finished"); - sendLogBroadcast(LOG_LEVEL_ERROR, "Disconnected"); - } - terminateConnection(gatt, ERROR_DEVICE_DISCONNECTED); - return; - } if (mError > 0) { // error occurred if ((mError & ERROR_CONNECTION_STATE_MASK) > 0) { final int error = mError & ~ERROR_CONNECTION_STATE_MASK; - loge("An error occurred while connecting to the device:" + error); - sendLogBroadcast(LOG_LEVEL_ERROR, String.format(Locale.US, "Connection failed (0x%02X): %s", error, GattError.parseConnectionError(error))); + final boolean timeout = error == 133 && after > before + 25000; // timeout is 30 sec + if (timeout) { + loge("Device not reachable. Check if the device with address " + deviceAddress + " is in range, is advertising and is connectable"); + sendLogBroadcast(LOG_LEVEL_ERROR, "Error 133: Connection timeout"); + } else { + loge("An error occurred while connecting to the device:" + error); + sendLogBroadcast(LOG_LEVEL_ERROR, String.format(Locale.US, "Connection failed (0x%02X): %s", error, GattError.parseConnectionError(error))); + } } else { final int error = mError & ~ERROR_CONNECTION_MASK; loge("An error occurred during discovering services:" + error); sendLogBroadcast(LOG_LEVEL_ERROR, String.format(Locale.US, "Connection failed (0x%02X): %s", error, GattError.parse(error))); } // Connection usually fails due to a 133 error (device unreachable, or.. something else went wrong). - // Usually trying the same for the second time works. - if (intent.getIntExtra(EXTRA_ATTEMPT, 0) == 0) { + // Usually trying the same for the second time works. Let's try 2 times. + final int attempt = intent.getIntExtra(EXTRA_RECONNECTION_ATTEMPT, 0); + if (attempt < 2) { sendLogBroadcast(LOG_LEVEL_WARNING, "Retrying..."); if (mConnectionState != STATE_DISCONNECTED) { @@ -1197,13 +1313,18 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres logi("Restarting the service"); final Intent newIntent = new Intent(); newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE); - newIntent.putExtra(EXTRA_ATTEMPT, 1); + newIntent.putExtra(EXTRA_RECONNECTION_ATTEMPT, attempt + 1); startService(newIntent); return; } terminateConnection(gatt, mError); return; } + if (mConnectionState == STATE_DISCONNECTED) { + sendLogBroadcast(LOG_LEVEL_ERROR, "Disconnected"); + terminateConnection(gatt, ERROR_DEVICE_DISCONNECTED); + return; + } if (mAborted) { logw("Upload aborted"); sendLogBroadcast(LOG_LEVEL_WARNING, "Upload aborted"); @@ -1213,8 +1334,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } sendLogBroadcast(LOG_LEVEL_INFO, "Services discovered"); - // Reset the attempt counter - intent.putExtra(EXTRA_ATTEMPT, 0); + // Reset the reconnection attempt counter + intent.putExtra(EXTRA_RECONNECTION_ATTEMPT, 0); DfuService dfuService = null; try { @@ -1242,9 +1363,19 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres mProgressInfo.setProgress(PROGRESS_ABORTED); } catch (final DeviceDisconnectedException e) { sendLogBroadcast(LOG_LEVEL_ERROR, "Device has disconnected"); - // TODO reconnect n times? loge(e.getMessage()); close(gatt); + + final int attempt = intent.getIntExtra(EXTRA_DFU_ATTEMPT, 0); + final int limit = intent.getIntExtra(EXTRA_MAX_DFU_ATTEMPTS, 0); + if (attempt < limit) { + logi("Restarting the service (" + (attempt + 1) + " /" + limit + ")"); + final Intent newIntent = new Intent(); + newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE); + newIntent.putExtra(EXTRA_DFU_ATTEMPT, attempt + 1); + startService(newIntent); + return; + } report(ERROR_DEVICE_DISCONNECTED); } catch (final DfuException e) { int error = e.getErrorNumber(); @@ -1273,15 +1404,17 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Opens the binary input stream that returns the firmware image content. A Path to the file is given. + * Opens the binary input stream that returns the firmware image content. + * A Path to the file is given. * - * @param filePath the path to the HEX, BIN or ZIP file - * @param mimeType the file type - * @param mbrSize the size of MBR, by default 0x1000 - * @param types the content files types in ZIP - * @return the input stream with binary image content + * @param filePath the path to the HEX, BIN or ZIP file. + * @param mimeType the file type. + * @param mbrSize the size of MBR, by default 0x1000. + * @param types the content files types in ZIP. + * @return The input stream with binary image content. */ - private InputStream openInputStream(final String filePath, final String mimeType, final int mbrSize, final int types) throws IOException { + private InputStream openInputStream(@NonNull final String filePath, final String mimeType, final int mbrSize, final int types) + throws IOException { final InputStream is = new FileInputStream(filePath); if (MIME_TYPE_ZIP.equals(mimeType)) return new ArchiveInputStream(is, mbrSize, types); @@ -1293,13 +1426,14 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** * Opens the binary input stream. A Uri to the stream is given. * - * @param stream the Uri to the stream - * @param mimeType the file type - * @param mbrSize the size of MBR, by default 0x1000 - * @param types the content files types in ZIP - * @return the input stream with binary image content + * @param stream the Uri to the stream. + * @param mimeType the file type. + * @param mbrSize the size of MBR, by default 0x1000. + * @param types the content files types in ZIP. + * @return The input stream with binary image content. */ - private InputStream openInputStream(final Uri stream, final String mimeType, final int mbrSize, final int types) throws IOException { + private InputStream openInputStream(@NonNull final Uri stream, final String mimeType, final int mbrSize, final int types) + throws IOException { final InputStream is = getContentResolver().openInputStream(stream); if (MIME_TYPE_ZIP.equals(mimeType)) return new ArchiveInputStream(is, mbrSize, types); @@ -1320,15 +1454,17 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Opens the binary input stream that returns the firmware image content. A resource id in the res/raw is given. + * Opens the binary input stream that returns the firmware image content. + * A resource id in the res/raw is given. * - * @param resId the if of the resource file - * @param mimeType the file type - * @param mbrSize the size of MBR, by default 0x1000 - * @param types the content files types in ZIP - * @return the input stream with binary image content + * @param resId the if of the resource file. + * @param mimeType the file type. + * @param mbrSize the size of MBR, by default 0x1000. + * @param types the content files types in ZIP. + * @return The input stream with binary image content. */ - private InputStream openInputStream(final int resId, final String mimeType, final int mbrSize, final int types) throws IOException { + private InputStream openInputStream(final int resId, final String mimeType, final int mbrSize, final int types) + throws IOException { final InputStream is = getResources().openRawResource(resId); if (MIME_TYPE_ZIP.equals(mimeType)) return new ArchiveInputStream(is, mbrSize, types); @@ -1341,13 +1477,15 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Connects to the BLE device with given address. This method is SYNCHRONOUS, it wait until the connection status change from {@link #STATE_CONNECTING} to {@link #STATE_CONNECTED_AND_READY} or an - * error occurs. This method returns null if Bluetooth adapter is disabled. + * Connects to the BLE device with given address. This method is SYNCHRONOUS, it wait until + * the connection status change from {@link #STATE_CONNECTING} to + * {@link #STATE_CONNECTED_AND_READY} or an error occurs. + * This method returns null if Bluetooth adapter is disabled. * - * @param address the device address - * @return the GATT device or null if Bluetooth adapter is disabled. + * @param address the device address. + * @return The GATT device or null if Bluetooth adapter is disabled. */ - protected BluetoothGatt connect(final String address) { + protected BluetoothGatt connect(@NonNull final String address) { if (!mBluetoothAdapter.isEnabled()) return null; @@ -1372,12 +1510,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Disconnects from the device and cleans local variables in case of error. This method is SYNCHRONOUS and wait until the disconnecting process will be completed. + * Disconnects from the device and cleans local variables in case of error. + * This method is SYNCHRONOUS and wait until the disconnecting process will be completed. * - * @param gatt the GATT device to be disconnected - * @param error error number + * @param gatt the GATT device to be disconnected. + * @param error error number. */ - protected void terminateConnection(final BluetoothGatt gatt, final int error) { + protected void terminateConnection(@NonNull final BluetoothGatt gatt, final int error) { if (mConnectionState != STATE_DISCONNECTED) { // Disconnect from the device disconnect(gatt); @@ -1392,12 +1531,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Disconnects from the device. This is SYNCHRONOUS method and waits until the callback returns new state. Terminates immediately if device is already disconnected. Do not call this method + * Disconnects from the device. This is SYNCHRONOUS method and waits until the callback returns + * new state. Terminates immediately if device is already disconnected. Do not call this method * directly, use {@link #terminateConnection(android.bluetooth.BluetoothGatt, int)} instead. * - * @param gatt the GATT device that has to be disconnected + * @param gatt the GATT device that has to be disconnected. */ - protected void disconnect(final BluetoothGatt gatt) { + protected void disconnect(@NonNull final BluetoothGatt gatt) { if (mConnectionState == STATE_DISCONNECTED) return; @@ -1415,7 +1555,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Wait until the connection state will change to {@link #STATE_DISCONNECTED} or until an error occurs. + * Wait until the connection state will change to {@link #STATE_DISCONNECTED} or until + * an error occurs. */ protected void waitUntilDisconnected() { try { @@ -1430,7 +1571,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** * Wait for given number of milliseconds. - * @param millis waiting period + * + * @param millis waiting period. */ protected void waitFor(final int millis) { synchronized (mLock) { @@ -1446,7 +1588,7 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** * Closes the GATT device and cleans up. * - * @param gatt the GATT device to be closed + * @param gatt the GATT device to be closed. */ protected void close(final BluetoothGatt gatt) { logi("Cleaning up..."); @@ -1456,10 +1598,11 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Clears the device cache. After uploading new firmware the DFU target will have other services than before. + * Clears the device cache. After uploading new firmware the DFU target will have other + * services than before. * - * @param gatt the GATT device to be refreshed - * @param force true to force the refresh + * @param gatt the GATT device to be refreshed. + * @param force true to force the refresh. */ protected void refreshDeviceCache(final BluetoothGatt gatt, final boolean force) { /* @@ -1474,11 +1617,10 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * There is a refresh() method in BluetoothGatt class but for now it's hidden. We will call it using reflections. */ try { + //noinspection JavaReflectionMemberAccess final Method refresh = gatt.getClass().getMethod("refresh"); - if (refresh != null) { - final boolean success = (Boolean) refresh.invoke(gatt); - logi("Refreshing result: " + success); - } + final boolean success = (Boolean) refresh.invoke(gatt); + logi("Refreshing result: " + success); } catch (Exception e) { loge("An exception occurred while refreshing device", e); sendLogBroadcast(LOG_LEVEL_WARNING, "Refreshing failed"); @@ -1487,7 +1629,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Creates or updates the notification in the Notification Manager. Sends broadcast with given progress state to the activity. + * Creates or updates the notification in the Notification Manager. Sends broadcast with + * given progress state to the activity. */ @Override public void updateProgressNotification() { @@ -1576,9 +1719,10 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /** * This method allows you to update the notification showing the upload progress. - * @param builder notification builder + * + * @param builder notification builder. */ - protected void updateProgressNotification(final NotificationCompat.Builder builder, final int progress) { + protected void updateProgressNotification(@NonNull final NotificationCompat.Builder builder, final int progress) { // Add Abort action to the notification if (progress != PROGRESS_ABORTED && progress != PROGRESS_COMPLETED) { final Intent abortIntent = new Intent(BROADCAST_ACTION); @@ -1589,9 +1733,10 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Creates or updates the notification in the Notification Manager. Sends broadcast with given error numbre to the activity. + * Creates or updates the notification in the Notification Manager. Sends broadcast with given + * error number to the activity. * - * @param error the error number + * @param error the error number. */ private void report(final int error) { sendErrorBroadcast(error); @@ -1633,7 +1778,8 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * This method allows you to update the notification showing an error. * @param builder error notification builder */ - protected void updateErrorNotification(final NotificationCompat.Builder builder) { + @SuppressWarnings("unused") + protected void updateErrorNotification(@NonNull final NotificationCompat.Builder builder) { // Empty default implementation } @@ -1666,10 +1812,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * This method allows you to update the notification that will be shown when the service goes to the foreground state. + * This method allows you to update the notification that will be shown when the service goes to + * the foreground state. + * * @param builder foreground notification builder */ - protected void updateForegroundNotification(final NotificationCompat.Builder builder) { + @SuppressWarnings("unused") + protected void updateForegroundNotification(@NonNull final NotificationCompat.Builder builder) { // Empty default implementation } @@ -1685,15 +1834,13 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * or error number if {@link #ERROR_MASK} bit set. * *

    - * The {@link #EXTRA_PROGRESS} is not set when a notification indicating a foreground service - * was clicked and notifications were disabled using {@link DfuServiceInitiator#setDisableNotification(boolean)}. - *

    + * The {@link #EXTRA_PROGRESS} is not set when a notification indicating a foreground service + * was clicked and notifications were disabled using {@link DfuServiceInitiator#setDisableNotification(boolean)}. *

    - * If your application disabled DFU notifications by calling + * If your application disabled DFU notifications by calling * {@link DfuServiceInitiator#setDisableNotification(boolean)} with parameter true this method * will still be called if the service was started as foreground service. To disable foreground service * call {@link DfuServiceInitiator#setForeground(boolean)} with parameter false. - *

    * _______________________________
    * * - connection state constants: * * - * @return the target activity class + * @return The target activity class. */ + @Nullable protected abstract Class getNotificationTarget(); /** @@ -1719,11 +1867,12 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres * return BuildConfig.DEBUG; * } * - * @return true to enable LogCat output, false (default) if not + * @return True to enable LogCat output, false (default) if not. */ protected boolean isDebug() { // Override this method and return true if you need more logs in LogCat - // Note: BuildConfig.DEBUG always returns false in library projects, so please use your app package BuildConfig + // Note: BuildConfig.DEBUG always returns false in library projects, so please use + // your app package BuildConfig return false; } @@ -1767,10 +1916,11 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } /** - * Initializes bluetooth adapter + * Initializes bluetooth adapter. * - * @return true if initialization was successful + * @return True if initialization was successful. */ + @SuppressWarnings("UnusedReturnValue") private boolean initialize() { // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothManager. @@ -1790,7 +1940,7 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } private void loge(final String message) { - Log.e(TAG, message); + Log.e(TAG, message); } private void loge(final String message, final Throwable e) { diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuCallback.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuCallback.java index 86c6fd5..c5dbb5d 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuCallback.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuCallback.java @@ -32,9 +32,15 @@ import android.bluetooth.BluetoothGattCallback; } } - /** Returns the final BluetoothGattCallback instance, depending on the implementation. */ + /** + * Returns the final BluetoothGattCallback instance, depending on the implementation. + */ DfuGattCallback getGattCallback(); - /** Callback invoked when bond state changes to {@link android.bluetooth.BluetoothDevice#BOND_BONDED BOND_BONDED} or {@link android.bluetooth.BluetoothDevice#BOND_NONE BOND_NONE}. */ + /** + * Callback invoked when bond state changes to + * {@link android.bluetooth.BluetoothDevice#BOND_BONDED BOND_BONDED} or + * {@link android.bluetooth.BluetoothDevice#BOND_NONE BOND_NONE}. + */ void onBondStateChanged(final int state); } diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuController.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuController.java index 0348053..8c34cf1 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuController.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuController.java @@ -22,7 +22,7 @@ package no.nordicsemi.android.dfu; -/* package */ public interface DfuController { +public interface DfuController { /** * Pauses the DFU operation. Call {@link #resume()} to resume, or {@link #abort()} to cancel. diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuLogListener.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuLogListener.java index 81b0cdc..40e8bcb 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuLogListener.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuLogListener.java @@ -23,12 +23,15 @@ package no.nordicsemi.android.dfu; /** - * Listener for log events. This listener should be used instead of creating the BroadcastReceiver on your own. + * Listener for log events. This listener should be used instead of creating the + * BroadcastReceiver on your own. + * * @see DfuServiceListenerHelper */ public interface DfuLogListener { /** * Method called when a log event was sent from the DFU service. + * * @param deviceAddress the target device address * @param level the log level, one of: *