Merge pull request #170 from NordicSemiconductor/feature/retries

New feature: retrying DFU in case of undesired disconnection
This commit is contained in:
Aleksander Nowakowski 2019-02-21 10:58:31 +01:00 committed by GitHub
commit bbecebe9a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1497 additions and 865 deletions

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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.

View File

@ -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;
/**
* <p>
* Flag set to <code>true</code> when the DFU target had send a notification with status other than success. Setting it to <code>true</code> will abort sending firmware and
* Flag set to <code>true</code> when the DFU target had send a notification with status other
* than success. Setting it to <code>true</code> will abort sending firmware and
* stop logging notifications (read below for explanation).
* </p>
* <p>
* 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.
* </p>
* 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.
* <p>
* 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.
* </p>
* 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.
* <p>
* N* - Value of Packet Receipt Notification, 12 by default.
* </p>
*/
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.
*/
/*

View File

@ -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 <code>true</code> if Service Changed CCCD is enabled and set to INDICATE
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @return <code>true</code> 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 <code>true</code> if operation succeeded, <code>false</code> otherwise
* @return <code>true</code> if operation succeeded, <code>false</code> 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 "";

View File

@ -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];
}

View File

@ -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.
* <p>
* 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");

View File

@ -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.
* <p>
* An application that supports one of those services should have the Secure DFU Service with one
* of those characteristic inside.
* <p>
* 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.
* <p>
* 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)");

View File

@ -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);
}

View File

@ -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.

View File

@ -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:
* <ul>

View File

@ -54,12 +54,13 @@ import androidx.annotation.NonNull;
return this;
}
@SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
DfuProgressInfo setTotalPart(final int totalParts) {
this.totalParts = totalParts;
return this;
}
public void setProgress(final int progress) {
void setProgress(final int progress) {
this.progress = progress;
mListener.updateProgressNotification();
}
@ -108,10 +109,12 @@ import androidx.annotation.NonNull;
return bytesSent;
}
@SuppressWarnings("unused")
int getBytesReceived() {
return bytesReceived;
}
@SuppressWarnings("unused")
int getImageSizeInBytes() {
return imageSizeInBytes;
}

View File

@ -22,92 +22,120 @@
package no.nordicsemi.android.dfu;
import androidx.annotation.NonNull;
/**
* Listener for status, progress and error events. This listener should be used instead of creating the BroadcastReceiver on your own.
* Listener for status, progress and error events. This listener should be used instead of
* creating the BroadcastReceiver on your own.
*
* @see DfuServiceListenerHelper
*/
public interface DfuProgressListener {
/**
* Method called when the DFU service started connecting with the DFU target.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onDeviceConnecting(final String deviceAddress);
void onDeviceConnecting(@NonNull final String deviceAddress);
/**
* Method called when the service has successfully connected, discovered services and found DFU service on the DFU target.
* @param deviceAddress the target device address
* Method called when the service has successfully connected, discovered services and found
* DFU service on the DFU target.
*
* @param deviceAddress the target device address.
*/
void onDeviceConnected(final String deviceAddress);
void onDeviceConnected(@NonNull final String deviceAddress);
/**
* Method called when the DFU process is starting. This includes reading the DFU Version characteristic, sending DFU_START command as well as the Init packet, if set.
* @param deviceAddress the target device address
* Method called when the DFU process is starting. This includes reading the DFU Version
* characteristic, sending DFU_START command as well as the Init packet, if set.
*
* @param deviceAddress the target device address.
*/
void onDfuProcessStarting(final String deviceAddress);
void onDfuProcessStarting(@NonNull final String deviceAddress);
/**
* Method called when the DFU process was started and bytes about to be sent.
*
* @param deviceAddress the target device address
*/
void onDfuProcessStarted(final String deviceAddress);
void onDfuProcessStarted(@NonNull final String deviceAddress);
/**
* Method called when the service discovered that the DFU target is in the application mode and must be switched to DFU mode.
* The switch command will be sent and the DFU process should start again. There will be no {@link #onDeviceDisconnected(String)} event following this call.
* @param deviceAddress the target device address
* Method called when the service discovered that the DFU target is in the application mode
* and must be switched to DFU mode. The switch command will be sent and the DFU process
* should start again. There will be no {@link #onDeviceDisconnected(String)} event following
* this call.
*
* @param deviceAddress the target device address.
*/
void onEnablingDfuMode(final String deviceAddress);
void onEnablingDfuMode(@NonNull final String deviceAddress);
/**
* Method called during uploading the firmware. It will not be called twice with the same value of percent, however, in case of small firmware files, some values may be omitted.
* @param deviceAddress the target device address
* @param percent the current status of upload (0-99)
* @param speed the current speed in bytes per millisecond
* @param avgSpeed the average speed in bytes per millisecond
* @param currentPart the number pf part being sent. In case the ZIP file contains a Soft Device and/or a Bootloader together with the application the SD+BL are sent as part 1,
* then the service starts again and send the application as part 2
* @param partsTotal total number of parts
* Method called during uploading the firmware. It will not be called twice with the same
* value of percent, however, in case of small firmware files, some values may be omitted.
*
* @param deviceAddress the target device address.
* @param percent the current status of upload (0-99).
* @param speed the current speed in bytes per millisecond.
* @param avgSpeed the average speed in bytes per millisecond
* @param currentPart the number pf part being sent. In case the ZIP file contains a Soft Device
* and/or a Bootloader together with the application the SD+BL are sent as part 1,
* then the service starts again and send the application as part 2.
* @param partsTotal total number of parts.
*/
void onProgressChanged(final String deviceAddress, final int percent, final float speed, final float avgSpeed, final int currentPart, final int partsTotal);
void onProgressChanged(@NonNull final String deviceAddress, final int percent,
final float speed, final float avgSpeed,
final int currentPart, final int partsTotal);
/**
* Method called when the new firmware is being validated on the target device.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onFirmwareValidating(final String deviceAddress);
void onFirmwareValidating(@NonNull final String deviceAddress);
/**
* Method called when the service started to disconnect from the target device.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onDeviceDisconnecting(final String deviceAddress);
/**
* Method called when the service disconnected from the device. The device has been reset.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onDeviceDisconnected(final String deviceAddress);
void onDeviceDisconnected(@NonNull final String deviceAddress);
/**
* Method called when the DFU process succeeded.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onDfuCompleted(final String deviceAddress);
void onDfuCompleted(@NonNull final String deviceAddress);
/**
* Method called when the DFU process has been aborted.
* @param deviceAddress the target device address
*
* @param deviceAddress the target device address.
*/
void onDfuAborted(final String deviceAddress);
void onDfuAborted(@NonNull final String deviceAddress);
/**
* Method called when an error occur.
* @param deviceAddress the target device address
* @param error error number
* @param errorType the error type, one of {@link DfuBaseService#ERROR_TYPE_COMMUNICATION_STATE}, {@link DfuBaseService#ERROR_TYPE_COMMUNICATION},
* {@link DfuBaseService#ERROR_TYPE_DFU_REMOTE}, {@link DfuBaseService#ERROR_TYPE_OTHER}.
* @param message the error message
*
* @param deviceAddress the target device address.
* @param error error number.
* @param errorType the error type, one of
* {@link DfuBaseService#ERROR_TYPE_COMMUNICATION_STATE},
* {@link DfuBaseService#ERROR_TYPE_COMMUNICATION},
* {@link DfuBaseService#ERROR_TYPE_DFU_REMOTE},
* {@link DfuBaseService#ERROR_TYPE_OTHER}.
* @param message the error message.
*/
void onError(final String deviceAddress, final int error, final int errorType, final String message);
void onError(@NonNull final String deviceAddress,
final int error, final int errorType, final String message);
}

View File

@ -22,39 +22,44 @@
package no.nordicsemi.android.dfu;
import androidx.annotation.NonNull;
public class DfuProgressListenerAdapter implements DfuProgressListener {
@Override
public void onDeviceConnecting(final String deviceAddress) {
public void onDeviceConnecting(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onDeviceConnected(final String deviceAddress) {
public void onDeviceConnected(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onDfuProcessStarting(final String deviceAddress) {
public void onDfuProcessStarting(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onDfuProcessStarted(final String deviceAddress) {
public void onDfuProcessStarted(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onEnablingDfuMode(final String deviceAddress) {
public void onEnablingDfuMode(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onProgressChanged(final String deviceAddress, final int percent, final float speed, final float avgSpeed, final int currentPart, final int partsTotal) {
public void onProgressChanged(@NonNull final String deviceAddress, final int percent,
final float speed, final float avgSpeed,
final int currentPart, final int partsTotal) {
// empty default implementation
}
@Override
public void onFirmwareValidating(final String deviceAddress) {
public void onFirmwareValidating(@NonNull final String deviceAddress) {
// empty default implementation
}
@ -64,22 +69,23 @@ public class DfuProgressListenerAdapter implements DfuProgressListener {
}
@Override
public void onDeviceDisconnected(final String deviceAddress) {
public void onDeviceDisconnected(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onDfuCompleted(final String deviceAddress) {
public void onDfuCompleted(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onDfuAborted(final String deviceAddress) {
public void onDfuAborted(@NonNull final String deviceAddress) {
// empty default implementation
}
@Override
public void onError(final String deviceAddress, final int error, final int errorType, final String message) {
public void onError(@NonNull final String deviceAddress,
final int error, final int errorType, final String message) {
// empty default implementation
}
}

View File

@ -0,0 +1,11 @@
package no.nordicsemi.android.dfu;
import androidx.annotation.IntDef;
@SuppressWarnings("WeakerAccess")
@IntDef(value = {
DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS,
DfuServiceInitiator.SCOPE_APPLICATION
},
flag = true)
public @interface DfuScope {}

View File

@ -27,24 +27,55 @@ import android.content.Intent;
import java.io.InputStream;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException;
import no.nordicsemi.android.dfu.internal.exception.DfuException;
import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
/* package */ interface DfuService extends DfuCallback {
/** This method must return true if the device is compatible with this DFU implementation, false otherwise. */
boolean isClientCompatible(final Intent intent, final BluetoothGatt gatt) throws DfuException, DeviceDisconnectedException, UploadAbortedException;
/**
* This method must return true if the device is compatible with this DFU implementation,
* false otherwise.
*
* @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.
*/
boolean isClientCompatible(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt)
throws DfuException, DeviceDisconnectedException, UploadAbortedException;
/**
* Initializes the DFU implementation and does some initial setting up.
* @return true if initialization was successful and the DFU process may begin, false to finish teh DFU service
*
* @return True if initialization was successful and the DFU process may begin,
* false to finish teh DFU service.
* @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.
*/
boolean initialize(final Intent intent, final BluetoothGatt gatt, final int fileType, final InputStream firmwareStream, final InputStream initPacketStream) throws DfuException, DeviceDisconnectedException, UploadAbortedException;
boolean initialize(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt,
@FileType final int fileType,
@NonNull final InputStream firmwareStream,
@Nullable final InputStream initPacketStream)
throws DfuException, DeviceDisconnectedException, UploadAbortedException;
/** Performs the DFU process. */
void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException;
/**
* Performs the DFU process.
*
* @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.
*/
void performDfu(@NonNull final Intent intent)
throws DfuException, DeviceDisconnectedException, UploadAbortedException;
/** Releases the service. */
/**
* Releases the service.
*/
void release();
}

View File

@ -24,13 +24,18 @@ package no.nordicsemi.android.dfu;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* A controller class allows you to pause, resume or abort the DFU operation in a easy way.
* <p>Keep in mind that there may be only one DFU operation at a time, and other instances of a DfuServiceController (for example obtained with a previous DFU)
* will work for all DFU processes, but the {@link #isPaused()} and {@link #isAborted()} methods may report incorrect values.</p>
* <p>Added in DFU Library version 1.0.2.</p>
* <p>
* Keep in mind that there may be only one DFU operation at a time, and other instances of
* a DfuServiceController (for example obtained with a previous DFU) will work for all DFU processes,
* but the {@link #isPaused()} and {@link #isAborted()} methods may report incorrect values.
* <p>
* Added in DFU Library version 1.0.2.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class DfuServiceController implements DfuController {
@ -38,7 +43,7 @@ public class DfuServiceController implements DfuController {
private boolean mPaused;
private boolean mAborted;
/* package */ DfuServiceController(final Context context) {
/* package */ DfuServiceController(@NonNull final Context context) {
mBroadcastManager = LocalBroadcastManager.getInstance(context);
}

View File

@ -33,8 +33,11 @@ import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
import android.os.Parcelable;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RawRes;
import androidx.annotation.RequiresApi;
import java.security.InvalidParameterException;
@ -45,14 +48,15 @@ import java.util.UUID;
* parameters to the service. The DfuServiceInitiator class may be used to make this process easier.
* It provides simple API that covers all low lever operations.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class DfuServiceInitiator {
@SuppressWarnings({"WeakerAccess", "unused", "deprecation"})
public final class DfuServiceInitiator {
public static final int DEFAULT_PRN_VALUE = 12;
public static final int DEFAULT_MBR_SIZE = 0x1000;
/** Constant used to narrow the scope of the update to system components (SD+BL) only. */
public static final int SCOPE_SYSTEM_COMPONENTS = 7578;
public static final int SCOPE_SYSTEM_COMPONENTS = 1;
/** Constant used to narrow the scope of the update to application only. */
public static final int SCOPE_APPLICATION = 3542;
public static final int SCOPE_APPLICATION = 2;
private final String deviceAddress;
private String deviceName;
@ -76,6 +80,8 @@ public class DfuServiceInitiator {
private boolean forceDfu = false;
private boolean enableUnsafeExperimentalButtonlessDfu = false;
private boolean disableResume = false;
private int numberOfRetries = 0; // 0 to be backwards compatible
private int mbrSize = DEFAULT_MBR_SIZE;
private Boolean packetReceiptNotificationsEnabled;
private int numberOfPackets = 12;
@ -195,13 +201,17 @@ public class DfuServiceInitiator {
/**
* If Packet Receipt Notification procedure is enabled, this method sets number of packets to
* be sent before receiving a PRN. A PRN is used to synchronize the transmitter and receiver.
* <p>
* If the value given is equal to 0, the {@link #DEFAULT_PRN_VALUE} will be used instead.
* <p>
* To disable PRNs use {@link #setPacketsReceiptNotificationsEnabled(boolean)}.
*
* @param number number of packets to be sent before receiving a PRN. Defaulted when set to 0.
* @return the builder
* @see #setPacketsReceiptNotificationsEnabled(boolean)
* @see DfuSettingsConstants#SETTINGS_NUMBER_OF_PACKETS
*/
public DfuServiceInitiator setPacketsReceiptNotificationsValue(final int number) {
public DfuServiceInitiator setPacketsReceiptNotificationsValue(@IntRange(from = 0) final int number) {
this.numberOfPackets = number > 0 ? number : DEFAULT_PRN_VALUE;
return this;
}
@ -262,6 +272,28 @@ public class DfuServiceInitiator {
return this;
}
/**
* Sets the number of retries that the DFU service will use to complete DFU. The default
* value is 0, for backwards compatibility reason.
* <p>
* If the given value is greater than 0, the service will restart itself at most {@code max}
* times in case of an undesired disconnection during DFU operation. This attempt counter
* is independent from another counter, for reconnection attempts, which is equal to 3.
* The latter one will be used when connection will fail with an error (possible packet
* collision or any other reason). After successful connection, the reconnection counter is
* reset, while the retry counter is cleared after a DFU finishes with success.
* <p>
* The service will not try to retry DFU in case of any other error, for instance an error
* sent from the target device.
*
* @param max Maximum number of retires to complete DFU. Usually around 2.
* @return the builder
*/
public DfuServiceInitiator setNumberOfRetries(@IntRange(from = 0) final int max) {
this.numberOfRetries = max;
return this;
}
/**
* Sets the Maximum Transfer Unit (MTU) value that the Secure DFU service will try to request
* before performing DFU. By default, value 517 will be used, which is the highest supported
@ -282,7 +314,7 @@ public class DfuServiceInitiator {
* @param mtu the MTU that wil be requested, 0 to disable MTU request.
* @return the builder
*/
public DfuServiceInitiator setMtu(final int mtu) {
public DfuServiceInitiator setMtu(@IntRange(from = 23, to = 517) final int mtu) {
this.mtu = mtu;
return this;
}
@ -304,7 +336,7 @@ public class DfuServiceInitiator {
* {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(BluetoothDevice, int)}.
* @return the builder
*/
public DfuServiceInitiator setCurrentMtu(final int mtu) {
public DfuServiceInitiator setCurrentMtu(@IntRange(from = 23, to = 517) final int mtu) {
this.currentMtu = mtu;
return this;
}
@ -331,17 +363,45 @@ public class DfuServiceInitiator {
* {@link #SCOPE_APPLICATION}.
* @return the builder
*/
public DfuServiceInitiator setScope(final int scope) {
public DfuServiceInitiator setScope(@DfuScope final int scope) {
if (!DfuBaseService.MIME_TYPE_ZIP.equals(mimeType))
throw new UnsupportedOperationException("Scope can be set only for a ZIP file");
if (scope == SCOPE_APPLICATION)
fileType = DfuBaseService.TYPE_APPLICATION;
else if (scope == SCOPE_SYSTEM_COMPONENTS)
fileType = DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER;
else if (scope == (SCOPE_APPLICATION | SCOPE_SYSTEM_COMPONENTS))
fileType = DfuBaseService.TYPE_AUTO;
else throw new UnsupportedOperationException("Unknown scope");
return this;
}
/**
* This method sets the size of an MBR (Master Boot Record). It should be used only
* when updating a file from a HEX file. If you use BIN or ZIP, value set here will
* be ignored.
* <p>
* The MBR size is important for the HEX parser, which has to cut it from the Soft Device's
* HEX before sending it to the DFU target. The MBR can't be updated using DFU, and the
* bootloader expects only the Soft Device bytes. Usually, the Soft Device HEX provided
* by Nordic contains an MBR at addresses 0x0000 to 0x1000.
* 0x1000 is the default size of MBR which will be used.
* <p>
* If you have a HEX file which address start from 0 and want to send the whole BIN content
* from it, you have to set the MBR size to 0, otherwise first 4096 bytes will be cut off.
* <p>
* The value set here will not be used if the {@link DfuSettingsConstants#SETTINGS_MBR_SIZE}
* is set in Shared Preferences.
*
* @param mbrSize the MBR size in bytes. Defaults to 4096 (0x1000) bytes.
* @return the builder
* @see DfuSettingsConstants#SETTINGS_MBR_SIZE
*/
public DfuServiceInitiator setMbrSize(@IntRange(from = 0) final int mbrSize) {
this.mbrSize = mbrSize;
return this;
}
/**
* Set this flag to true to enable experimental buttonless feature in Secure DFU. When the
* experimental Buttonless DFU Service is found on a device, the service will use it to
@ -369,9 +429,11 @@ public class DfuServiceInitiator {
* 0x01 - Success<br>
* The device should disconnect and restart in DFU mode after sending the notification.
* <p>
* 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.
* The Buttonless service has changed in SDK 13 and later. Indications are used instead of
* notifications. Also, Buttonless service for bonded devices has been added.
* It is recommended to use any of the new services instead.
*
* @return the builder
*/
public DfuServiceInitiator setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(final boolean enable) {
this.enableUnsafeExperimentalButtonlessDfu = enable;
@ -526,7 +588,7 @@ public class DfuServiceInitiator {
* @see #setZip(Uri)
* @see #setZip(String)
*/
public DfuServiceInitiator setZip(final int rawResId) {
public DfuServiceInitiator setZip(@RawRes final int rawResId) {
return init(null, null, rawResId, DfuBaseService.TYPE_AUTO, DfuBaseService.MIME_TYPE_ZIP);
}
@ -558,7 +620,7 @@ public class DfuServiceInitiator {
* @return the builder
*/
@Deprecated
public DfuServiceInitiator setBinOrHex(final int fileType, @NonNull final Uri uri) {
public DfuServiceInitiator setBinOrHex(@FileType final int fileType, @NonNull final Uri uri) {
if (fileType == DfuBaseService.TYPE_AUTO)
throw new UnsupportedOperationException("You must specify the file type");
return init(uri, null, 0, fileType, DfuBaseService.MIME_TYPE_OCTET_STREAM);
@ -574,7 +636,7 @@ public class DfuServiceInitiator {
* @return the builder
*/
@Deprecated
public DfuServiceInitiator setBinOrHex(final int fileType, @NonNull final String path) {
public DfuServiceInitiator setBinOrHex(@FileType final int fileType, @NonNull final String path) {
if (fileType == DfuBaseService.TYPE_AUTO)
throw new UnsupportedOperationException("You must specify the file type");
return init(null, path, 0, fileType, DfuBaseService.MIME_TYPE_OCTET_STREAM);
@ -592,7 +654,7 @@ public class DfuServiceInitiator {
* @deprecated The Distribution packet (ZIP) should be used for DFU Bootloader version 0.5 or newer
*/
@Deprecated
public DfuServiceInitiator setBinOrHex(final int fileType, @Nullable final Uri uri, @Nullable final String path) {
public DfuServiceInitiator setBinOrHex(@FileType final int fileType, @Nullable final Uri uri, @Nullable final String path) {
if (fileType == DfuBaseService.TYPE_AUTO)
throw new UnsupportedOperationException("You must specify the file type");
return init(uri, path, 0, fileType, DfuBaseService.MIME_TYPE_OCTET_STREAM);
@ -608,7 +670,7 @@ public class DfuServiceInitiator {
* @return the builder
*/
@Deprecated
public DfuServiceInitiator setBinOrHex(final int fileType, final int rawResId) {
public DfuServiceInitiator setBinOrHex(@FileType final int fileType, @RawRes final int rawResId) {
if (fileType == DfuBaseService.TYPE_AUTO)
throw new UnsupportedOperationException("You must specify the file type");
return init(null, null, rawResId, fileType, DfuBaseService.MIME_TYPE_OCTET_STREAM);
@ -649,7 +711,7 @@ public class DfuServiceInitiator {
* @return the builder
*/
@Deprecated
public DfuServiceInitiator setInitFile(final int initFileResId) {
public DfuServiceInitiator setInitFile(@RawRes final int initFileResId) {
return init(null, null, initFileResId);
}
@ -695,6 +757,8 @@ public class DfuServiceInitiator {
intent.putExtra(DfuBaseService.EXTRA_RESTORE_BOND, restoreBond);
intent.putExtra(DfuBaseService.EXTRA_FORCE_DFU, forceDfu);
intent.putExtra(DfuBaseService.EXTRA_DISABLE_RESUME, disableResume);
intent.putExtra(DfuBaseService.EXTRA_MAX_DFU_ATTEMPTS, numberOfRetries);
intent.putExtra(DfuBaseService.EXTRA_MBR_SIZE, mbrSize);
if (mtu > 0)
intent.putExtra(DfuBaseService.EXTRA_MTU, mtu);
intent.putExtra(DfuBaseService.EXTRA_CURRENT_MTU, currentMtu);
@ -732,7 +796,7 @@ public class DfuServiceInitiator {
private DfuServiceInitiator init(@Nullable final Uri initFileUri,
@Nullable final String initFilePath,
final int initFileResId) {
@RawRes final int initFileResId) {
if (DfuBaseService.MIME_TYPE_ZIP.equals(mimeType))
throw new InvalidParameterException("Init file must be located inside the ZIP");
@ -744,7 +808,7 @@ public class DfuServiceInitiator {
private DfuServiceInitiator init(@Nullable final Uri fileUri,
@Nullable final String filePath,
final int fileResId, final int fileType,
@RawRes final int fileResId, @FileType final int fileType,
@NonNull final String mimeType) {
this.fileUri = fileUri;
this.filePath = filePath;

View File

@ -26,23 +26,27 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import no.nordicsemi.android.dfu.internal.scanner.BootloaderScanner;
import no.nordicsemi.android.error.GattError;
/**
* A helper class that should be used to register listeners for DFU Service broadcast events.
* The {@link DfuProgressListener} should be registered to listen for DFU status updates and errors, while the {@link DfuLogListener} listener receives the log updates.
* Listeners may be registered for a specified device (given with device address) or for any device. Keep in mind, that while updating the SoftDevice using the buttonless update
* the device may change its address in the bootloader mode.
*
* <p>Use {@link #registerProgressListener(Context, DfuProgressListener)} or {@link #registerLogListener(Context, DfuLogListener)} to register your listeners. Remember about unregistering them
* when your context is destroyed.</p>
* The {@link DfuProgressListener} should be registered to listen for DFU status updates and errors,
* while the {@link DfuLogListener} listener receives the log updates.
* Listeners may be registered for a specified device (given with device address) or for any device.
* Keep in mind, that while updating the SoftDevice using the buttonless update the device may
* change its address in the bootloader mode.
* <p>
* Use {@link #registerProgressListener(Context, DfuProgressListener)} or
* {@link #registerLogListener(Context, DfuLogListener)} to register your listeners.
* Remember about unregistering them when your context is destroyed.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class DfuServiceListenerHelper {
@ -58,8 +62,10 @@ public class DfuServiceListenerHelper {
}
private void setLogListener(final String deviceAddress, final DfuLogListener listener) {
// When using the buttonless update and updating the SoftDevice the application will be removed to make space for the new SoftDevice.
// The new bootloader will afterwards advertise with the address incremented by 1. We need to make sure that the listener will receive also events from this device.
// When using the buttonless update and updating the SoftDevice the application will
// be removed to make space for the new SoftDevice.
// The new bootloader will afterwards advertise with the address incremented by 1.
// We need to make sure that the listener will receive also events from this device.
mListeners.put(deviceAddress, listener);
mListeners.put(getIncrementedAddress(deviceAddress), listener); // assuming the address is a valid BLE address
}
@ -154,6 +160,8 @@ public class DfuServiceListenerHelper {
return;
final String action = intent.getAction();
if (action == null)
return;
switch (action) {
case DfuBaseService.BROADCAST_PROGRESS: {
@ -268,11 +276,13 @@ public class DfuServiceListenerHelper {
}
/**
* Registers the {@link DfuProgressListener}. Registered listener will receive the progress events from the DFU service.
* @param context the application context
* @param listener the listener to register
* Registers the {@link DfuProgressListener}.
* Registered listener will receive the progress events from the DFU service.
*
* @param context the application context.
* @param listener the listener to register.
*/
public static void registerProgressListener(final Context context, final DfuProgressListener listener) {
public static void registerProgressListener(@NonNull final Context context, @NonNull final DfuProgressListener listener) {
if (mProgressBroadcastReceiver == null) {
mProgressBroadcastReceiver = new ProgressBroadcastsReceiver();
@ -285,12 +295,15 @@ public class DfuServiceListenerHelper {
}
/**
* Registers the {@link DfuProgressListener}. Registered listener will receive the progress events from the DFU service.
* @param context the application context
* @param listener the listener to register
* @param deviceAddress the address of the device to receive updates from (or null if any device)
* Registers the {@link DfuProgressListener}. Registered listener will receive the progress
* events from the DFU service.
*
* @param context the application context.
* @param listener the listener to register.
* @param deviceAddress the address of the device to receive updates from (or null if any device).
*/
public static void registerProgressListener(final Context context, final DfuProgressListener listener, final String deviceAddress) {
public static void registerProgressListener(@NonNull final Context context,
@NonNull final DfuProgressListener listener, @NonNull final String deviceAddress) {
if (mProgressBroadcastReceiver == null) {
mProgressBroadcastReceiver = new ProgressBroadcastsReceiver();
@ -304,10 +317,11 @@ public class DfuServiceListenerHelper {
/**
* Unregisters the previously registered progress listener.
* @param context the application context
* @param listener the listener to unregister
*
* @param context the application context.
* @param listener the listener to unregister.
*/
public static void unregisterProgressListener(final Context context, final DfuProgressListener listener) {
public static void unregisterProgressListener(@NonNull final Context context, @NonNull final DfuProgressListener listener) {
if (mProgressBroadcastReceiver != null) {
final boolean empty = mProgressBroadcastReceiver.removeProgressListener(listener);
@ -320,10 +334,11 @@ public class DfuServiceListenerHelper {
/**
* Registers the {@link DfuLogListener}. Registered listener will receive the log events from the DFU service.
* @param context the application context
* @param listener the listener to register
*
* @param context the application context.
* @param listener the listener to register.
*/
public static void registerLogListener(final Context context, final DfuLogListener listener) {
public static void registerLogListener(@NonNull final Context context, @NonNull final DfuLogListener listener) {
if (mLogBroadcastReceiver == null) {
mLogBroadcastReceiver = new LogBroadcastReceiver();
@ -335,12 +350,15 @@ public class DfuServiceListenerHelper {
}
/**
* Registers the {@link DfuLogListener}. Registered listener will receive the log events from the DFU service.
* @param context the application context
* @param listener the listener to register
* @param deviceAddress the address of the device to receive updates from (or null if any device)
* Registers the {@link DfuLogListener}. Registered listener will receive the log events from
* the DFU service.
*
* @param context the application context.
* @param listener the listener to register.
* @param deviceAddress the address of the device to receive updates from (or null if any device).
*/
public static void registerLogListener(final Context context, final DfuLogListener listener, final String deviceAddress) {
public static void registerLogListener(@NonNull final Context context,
@NonNull final DfuLogListener listener, @NonNull final String deviceAddress) {
if (mLogBroadcastReceiver == null) {
mLogBroadcastReceiver = new LogBroadcastReceiver();
@ -353,10 +371,11 @@ public class DfuServiceListenerHelper {
/**
* Unregisters the previously registered log listener.
* @param context the application context
* @param listener the listener to unregister
*
* @param context the application context.
* @param listener the listener to unregister.
*/
public static void unregisterLogListener(final Context context, final DfuLogListener listener) {
public static void unregisterLogListener(@NonNull final Context context, @NonNull final DfuLogListener listener) {
if (mLogBroadcastReceiver != null) {
final boolean empty = mLogBroadcastReceiver.removeLogListener(listener);
@ -367,7 +386,7 @@ public class DfuServiceListenerHelper {
}
}
private static String getIncrementedAddress(final String deviceAddress) {
private static String getIncrementedAddress(@NonNull final String deviceAddress) {
final String firstBytes = deviceAddress.substring(0, 15);
final String lastByte = deviceAddress.substring(15); // assuming that the device address is correct
final String lastByteIncremented = String.format(Locale.US, "%02X", (Integer.valueOf(lastByte, 16) + BootloaderScanner.ADDRESS_DIFF) & 0xFF);

View File

@ -25,6 +25,7 @@ package no.nordicsemi.android.dfu;
import android.bluetooth.BluetoothGatt;
import android.content.Intent;
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;
@ -34,7 +35,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
private boolean mPaused;
private boolean mAborted;
/* package */ DfuService getServiceImpl(final Intent intent, final DfuBaseService service, final BluetoothGatt gatt) throws DfuException, DeviceDisconnectedException, UploadAbortedException {
DfuService getServiceImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service, @NonNull final BluetoothGatt gatt)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
try {
mImpl = new ButtonlessDfuWithBondSharingImpl(intent, service);
if (mImpl.isClientCompatible(intent, gatt))
@ -56,7 +58,8 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
if (mImpl.isClientCompatible(intent, gatt))
return mImpl;
// Support for experimental Buttonless DFU Service from SDK 12. This feature must be explicitly enabled in the initiator.
// Support for experimental Buttonless DFU Service from SDK 12.
// This feature must be explicitly enabled in the initiator.
final boolean enableUnsafeExperimentalButtonlessDfuService = intent.getBooleanExtra(DfuBaseService.EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU, false);
if (enableUnsafeExperimentalButtonlessDfuService) {
mImpl = new ExperimentalButtonlessDfuImpl(intent, service);

View File

@ -24,6 +24,14 @@ package no.nordicsemi.android.dfu;
import android.bluetooth.BluetoothGattCharacteristic;
/**
* A collection of constants used for reading DFU constants from
* {@link android.preference.PreferenceManager} in previous versions of DFU Library.
*
* @deprecated Use {@link DfuServiceInitiator} methods instead.
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public interface DfuSettingsConstants {
/**
* This property must contain a boolean value.
@ -90,15 +98,12 @@ public interface DfuSettingsConstants {
* If you are using the PC nrf util tool to create a ZIP Distribution Packet with the
* firmware and Init Packet this option does not apply as the nrf tool will convert
* HEX to BIN itself.
*
* @deprecated Use {@link DfuServiceInitiator#setMbrSize(int)} instead.
*/
@Deprecated
String SETTINGS_MBR_SIZE = "settings_mbr_size";
/**
* The default value of the MBR size.
* @see #SETTINGS_DEFAULT_MBR_SIZE
*/
int SETTINGS_DEFAULT_MBR_SIZE = 0x1000;
/**
* This property must contain a boolean value.
* <p>

View File

@ -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;
@ -41,21 +42,21 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
*/
/* package */ class ExperimentalButtonlessDfuImpl extends ButtonlessDfuImpl {
/** The UUID of the experimental Buttonless DFU service from SDK 12.x. */
protected static final UUID DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID = new UUID(0x8E400001F3154F60L, 0x9FB8838830DAEA50L);
static final UUID DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID = new UUID(0x8E400001F3154F60L, 0x9FB8838830DAEA50L);
/** The UUID of the experimental Buttonless DFU characteristic from SDK 12.x. */
protected static final UUID DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_UUID = new UUID(0x8E400001F3154F60L, 0x9FB8838830DAEA50L); // the same as service
static final UUID DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_UUID = new UUID(0x8E400001F3154F60L, 0x9FB8838830DAEA50L); // the same as service
protected static UUID EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID;
protected static UUID EXPERIMENTAL_BUTTONLESS_DFU_UUID = DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_UUID;
static UUID EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID = DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID;
static UUID EXPERIMENTAL_BUTTONLESS_DFU_UUID = DEFAULT_EXPERIMENTAL_BUTTONLESS_DFU_UUID;
private BluetoothGattCharacteristic mButtonlessDfuCharacteristic;
ExperimentalButtonlessDfuImpl(final Intent intent, final DfuBaseService service) {
ExperimentalButtonlessDfuImpl(@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(EXPERIMENTAL_BUTTONLESS_DFU_SERVICE_UUID);
if (dfuService == null)
return false;
@ -82,7 +83,7 @@ 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("Experimental buttonless service found -> SDK 12.x");
super.performDfu(intent);
}

View File

@ -0,0 +1,13 @@
package no.nordicsemi.android.dfu;
import androidx.annotation.IntDef;
@SuppressWarnings("WeakerAccess")
@IntDef(value = {
DfuBaseService.TYPE_SOFT_DEVICE,
DfuBaseService.TYPE_BOOTLOADER,
DfuBaseService.TYPE_APPLICATION
},
flag = true)
public @interface FileType {}

View File

@ -31,6 +31,7 @@ import android.preference.PreferenceManager;
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;
@ -40,21 +41,23 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
*/
/* package */ class LegacyButtonlessDfuImpl extends BaseButtonlessDfuImpl {
// UUIDs used by the DFU
protected static UUID DFU_SERVICE_UUID = LegacyDfuImpl.DEFAULT_DFU_SERVICE_UUID;
protected static UUID DFU_CONTROL_POINT_UUID = LegacyDfuImpl.DEFAULT_DFU_CONTROL_POINT_UUID;
protected static UUID DFU_VERSION_UUID = LegacyDfuImpl.DEFAULT_DFU_VERSION_UUID;
static UUID DFU_SERVICE_UUID = LegacyDfuImpl.DEFAULT_DFU_SERVICE_UUID;
static UUID DFU_CONTROL_POINT_UUID = LegacyDfuImpl.DEFAULT_DFU_CONTROL_POINT_UUID;
static UUID DFU_VERSION_UUID = LegacyDfuImpl.DEFAULT_DFU_VERSION_UUID;
private static final byte[] OP_CODE_ENTER_BOOTLOADER = new byte[] {0x01, 0x04};
private static final byte[] OP_CODE_ENTER_BOOTLOADER = new byte[]{0x01, 0x04};
private BluetoothGattCharacteristic mControlPointCharacteristic;
private int mVersion;
LegacyButtonlessDfuImpl(final Intent intent, final DfuBaseService service) {
LegacyButtonlessDfuImpl(@NonNull final Intent intent, @NonNull final DfuBaseService service) {
super(intent, service);
}
@SuppressWarnings("deprecation")
@Override
public boolean isClientCompatible(final Intent intent, final BluetoothGatt gatt) throws DfuException, DeviceDisconnectedException, UploadAbortedException {
public boolean isClientCompatible(@NonNull final Intent intent, @NonNull final BluetoothGatt gatt)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
final BluetoothGattService dfuService = gatt.getService(DFU_SERVICE_UUID);
if (dfuService == null)
return false;
@ -136,7 +139,7 @@ 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 {
logw("Application with legacy buttonless update found");
// The service is connected to the application, not to the bootloader
@ -196,14 +199,16 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
/**
* Reads the DFU Version characteristic if such exists. Otherwise it returns 0.
*
* @param gatt the GATT device
* @param characteristic the characteristic to read
* @return a version number or 0 if not present on the bootloader
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @param gatt the GATT device.
* @param characteristic the characteristic to read.
* @return a version number or 0 if not present on the bootloader.
* @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 int readVersion(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
private int readVersion(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic)
throws DeviceDisconnectedException, DfuException, UploadAbortedException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read version number: device disconnected");
if (mAborted)
@ -225,7 +230,7 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
// We have to wait until device receives a response or an error occur
try {
synchronized (mLock) {
while (((!mRequestCompleted || characteristic.getValue() == null ) && mConnected && mError == 0 && !mAborted) || mPaused) {
while (((!mRequestCompleted || characteristic.getValue() == null) && mConnected && mError == 0 && !mAborted) || mPaused) {
mRequestCompleted = false;
mLock.wait();
}
@ -233,10 +238,10 @@ import no.nordicsemi.android.dfu.internal.exception.UploadAbortedException;
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
if (mError != 0)
throw new DfuException("Unable to read version number", mError);
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read version number: device disconnected");
if (mError != 0)
throw new DfuException("Unable to read version number", mError);
// The version is a 16-bit unsigned int
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0);

View File

@ -31,6 +31,8 @@ import android.os.SystemClock;
import java.util.Locale;
import java.util.UUID;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import no.nordicsemi.android.dfu.internal.ArchiveInputStream;
import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException;
import no.nordicsemi.android.dfu.internal.exception.DfuException;
@ -41,15 +43,15 @@ import no.nordicsemi.android.error.LegacyDfuError;
/* package */ class LegacyDfuImpl extends BaseCustomDfuImpl {
// UUIDs used by the DFU
protected static final UUID DEFAULT_DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L);
protected static final UUID DEFAULT_DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L);
protected static final UUID DEFAULT_DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L);
protected static final UUID DEFAULT_DFU_VERSION_UUID = new UUID(0x000015341212EFDEL, 0x1523785FEABCD123L);
static final UUID DEFAULT_DFU_SERVICE_UUID = new UUID(0x000015301212EFDEL, 0x1523785FEABCD123L);
static final UUID DEFAULT_DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEL, 0x1523785FEABCD123L);
static final UUID DEFAULT_DFU_PACKET_UUID = new UUID(0x000015321212EFDEL, 0x1523785FEABCD123L);
static final UUID DEFAULT_DFU_VERSION_UUID = new UUID(0x000015341212EFDEL, 0x1523785FEABCD123L);
protected static UUID DFU_SERVICE_UUID = DEFAULT_DFU_SERVICE_UUID;
protected static UUID DFU_CONTROL_POINT_UUID = DEFAULT_DFU_CONTROL_POINT_UUID;
protected static UUID DFU_PACKET_UUID = DEFAULT_DFU_PACKET_UUID;
protected static UUID DFU_VERSION_UUID = DEFAULT_DFU_VERSION_UUID;
static UUID DFU_SERVICE_UUID = DEFAULT_DFU_SERVICE_UUID;
static UUID DFU_CONTROL_POINT_UUID = DEFAULT_DFU_CONTROL_POINT_UUID;
static UUID DFU_PACKET_UUID = DEFAULT_DFU_PACKET_UUID;
static UUID DFU_VERSION_UUID = DEFAULT_DFU_VERSION_UUID;
private static final int DFU_STATUS_SUCCESS = 1;
// Operation codes and packets
@ -124,12 +126,12 @@ import no.nordicsemi.android.error.LegacyDfuError;
}
}
/* package */ LegacyDfuImpl(final Intent intent, final DfuBaseService service) {
/* package */ LegacyDfuImpl(@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(DFU_SERVICE_UUID);
if (dfuService == null)
return false;
@ -162,7 +164,8 @@ import no.nordicsemi.android.error.LegacyDfuError;
}
@Override
public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException {
public void performDfu(@NonNull final Intent intent)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
logw("Legacy DFU bootloader found");
mProgressInfo.setProgress(DfuBaseService.PROGRESS_STARTING);
@ -474,7 +477,6 @@ import no.nordicsemi.android.error.LegacyDfuError;
} catch (final DeviceDisconnectedException e) {
loge("Disconnected while sending data");
throw e;
// TODO reconnect?
}
final long endTime = SystemClock.elapsedRealtime();
@ -550,10 +552,13 @@ import no.nordicsemi.android.error.LegacyDfuError;
/**
* Sets number of data packets that will be send before the notification will be received.
*
* @param data control point data packet
* @param value number of packets before receiving notification. If this value is 0, then the notification of packet receipt will be disabled by the DFU target.
* @param data control point data packet.
* @param value number of packets before receiving notification.
* If this value is 0, then the notification of packet receipt will be disabled
* by the DFU target.
*/
private void setNumberOfPackets(final byte[] data, final int value) {
@SuppressWarnings("SameParameterValue")
private void setNumberOfPackets(@NonNull final byte[] data, final int value) {
data[1] = (byte) (value & 0xFF);
data[2] = (byte) ((value >> 8) & 0xFF);
}
@ -562,12 +567,14 @@ import no.nordicsemi.android.error.LegacyDfuError;
* 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.
*/
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] < 1 || response[2] > 6)
private int getStatusCode(@Nullable 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] < 1 || response[2] > 6)
throw new UnknownResponseException("Invalid response received", response, OP_CODE_RESPONSE_CODE_KEY, request);
return response[2];
}
@ -575,10 +582,10 @@ import no.nordicsemi.android.error.LegacyDfuError;
/**
* Returns the DFU Version characteristic if such exists. Otherwise it returns 0.
*
* @param characteristic the characteristic to read
* @return a version number or 0 if not present on the bootloader
* @param characteristic the characteristic to read.
* @return a version number or 0 if not present on the bootloader.
*/
private int readVersion(final BluetoothGattCharacteristic characteristic) {
private int readVersion(@Nullable final BluetoothGattCharacteristic characteristic) {
// The value of this characteristic has been read before by LegacyButtonlessDfuImpl
return characteristic != null ? characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0) : 0;
}
@ -589,28 +596,34 @@ import no.nordicsemi.android.error.LegacyDfuError;
* 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
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @param characteristic the characteristic to write to. Should be the DFU CONTROL POINT.
* @param value the value to write to the characteristic.
* @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 writeOpCode(final BluetoothGattCharacteristic characteristic, final byte[] value) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
private void writeOpCode(@NonNull final BluetoothGattCharacteristic characteristic, @NonNull final byte[] value)
throws DeviceDisconnectedException, DfuException, UploadAbortedException {
final boolean reset = value[0] == OP_CODE_RESET_KEY || value[0] == OP_CODE_ACTIVATE_AND_RESET_KEY;
writeOpCode(characteristic, value, reset);
}
/**
* Writes the image size 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 image size 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 imageSize the image size in bytes
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @param characteristic the characteristic to write to. Should be the DFU PACKET.
* @param imageSize the image size in bytes.
* @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 writeImageSize(final BluetoothGattCharacteristic characteristic, final int imageSize) throws DeviceDisconnectedException, DfuException,
private void writeImageSize(@NonNull final BluetoothGattCharacteristic characteristic, final int imageSize)
throws DeviceDisconnectedException, DfuException,
UploadAbortedException {
mReceivedData = null;
mError = 0;
@ -634,31 +647,35 @@ import no.nordicsemi.android.error.LegacyDfuError;
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Image Size", mError);
if (!mConnected)
throw new DeviceDisconnectedException("Unable to write Image Size: device disconnected");
if (mError != 0)
throw new DfuException("Unable to write Image Size", mError);
}
/**
* <p>
* Writes the Soft Device, Bootloader and Application image sizes to the characteristic. Soft Device and Bootloader update is supported since Soft Device s110 v7.0.0.
* Sizes of SD, BL and App are uploaded as 3x UINT32 even though some of them may be 0s. F.e. if only App is being updated the data will be <0x00000000, 0x00000000, [App size]>
* </p>
* Writes the Soft Device, Bootloader and Application image sizes to the characteristic.
* Soft Device and Bootloader update is supported since Soft Device s110 v7.0.0.
* Sizes of SD, BL and App are uploaded as 3x UINT32 even though some of them may be 0s.
* E.g. if only App is being updated the data will be <0x00000000, 0x00000000, [App size]>
* <p>
* 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.
* </p>
* 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 softDeviceImageSize the Soft Device image size in bytes
* @param bootloaderImageSize the Bootloader image size in bytes
* @param appImageSize the Application image size in bytes
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @param characteristic the characteristic to write to. Should be the DFU PACKET.
* @param softDeviceImageSize the Soft Device image size in bytes.
* @param bootloaderImageSize the Bootloader image size in bytes.
* @param appImageSize the Application image size in bytes.
* @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 writeImageSize(final BluetoothGattCharacteristic characteristic, final int softDeviceImageSize, final int bootloaderImageSize, final int appImageSize)
private void writeImageSize(@NonNull final BluetoothGattCharacteristic characteristic,
final int softDeviceImageSize, final int bootloaderImageSize, final int appImageSize)
throws DeviceDisconnectedException, DfuException, UploadAbortedException {
mReceivedData = null;
mError = 0;
@ -684,21 +701,24 @@ import no.nordicsemi.android.error.LegacyDfuError;
}
if (mAborted)
throw new UploadAbortedException();
if (mError != 0)
throw new DfuException("Unable to write Image Sizes", mError);
if (!mConnected)
throw new DeviceDisconnectedException("Unable to write Image Sizes: device disconnected");
if (mError != 0)
throw new DfuException("Unable to write Image Sizes", mError);
}
/**
* Sends Reset command to the target device to reset its state and restarts the DFU Service that will start again.
* @param gatt the GATT device
* @param intent intent used to start the service
* @throws DfuException
* @throws DeviceDisconnectedException
* @throws UploadAbortedException
*
* @param gatt the GATT device.
* @param intent intent used to start the service.
* @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 resetAndRestart(final BluetoothGatt gatt, final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException {
private void resetAndRestart(@NonNull final BluetoothGatt gatt, @NonNull final Intent intent)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Last upload interrupted. Restarting device...");
// Send 'jump to bootloader command' (Start DFU)
mProgressInfo.setProgress(DfuBaseService.PROGRESS_DISCONNECTING);

View File

@ -36,6 +36,8 @@ import java.util.Locale;
import java.util.UUID;
import java.util.zip.CRC32;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import no.nordicsemi.android.dfu.internal.ArchiveInputStream;
import no.nordicsemi.android.dfu.internal.exception.DeviceDisconnectedException;
import no.nordicsemi.android.dfu.internal.exception.DfuException;
@ -49,13 +51,13 @@ import no.nordicsemi.android.error.SecureDfuError;
@SuppressWarnings("JavaDoc")
class SecureDfuImpl extends BaseCustomDfuImpl {
// UUIDs used by the DFU
protected static final UUID DEFAULT_DFU_SERVICE_UUID = new UUID(0x0000FE5900001000L, 0x800000805F9B34FBL); // 16-bit UUID assigned by Bluetooth SIG
protected static final UUID DEFAULT_DFU_CONTROL_POINT_UUID = new UUID(0x8EC90001F3154F60L, 0x9FB8838830DAEA50L);
protected static final UUID DEFAULT_DFU_PACKET_UUID = new UUID(0x8EC90002F3154F60L, 0x9FB8838830DAEA50L);
static final UUID DEFAULT_DFU_SERVICE_UUID = new UUID(0x0000FE5900001000L, 0x800000805F9B34FBL); // 16-bit UUID assigned by Bluetooth SIG
static final UUID DEFAULT_DFU_CONTROL_POINT_UUID = new UUID(0x8EC90001F3154F60L, 0x9FB8838830DAEA50L);
static final UUID DEFAULT_DFU_PACKET_UUID = new UUID(0x8EC90002F3154F60L, 0x9FB8838830DAEA50L);
protected static UUID DFU_SERVICE_UUID = DEFAULT_DFU_SERVICE_UUID;
protected static UUID DFU_CONTROL_POINT_UUID = DEFAULT_DFU_CONTROL_POINT_UUID;
protected static UUID DFU_PACKET_UUID = DEFAULT_DFU_PACKET_UUID;
static UUID DFU_SERVICE_UUID = DEFAULT_DFU_SERVICE_UUID;
static UUID DFU_CONTROL_POINT_UUID = DEFAULT_DFU_CONTROL_POINT_UUID;
static UUID DFU_PACKET_UUID = DEFAULT_DFU_PACKET_UUID;
private static final int DFU_STATUS_SUCCESS = 1;
private static final int MAX_ATTEMPTS = 3;
@ -109,7 +111,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
// If so, update the number of bytes received
mProgressInfo.setBytesReceived(offset);
} else {
// Else, and only in case it was a PRN received, not the response for Calculate Checksum Request, stop sending data
// Else, and only in case it was a PRN received, not the response for
// Calculate Checksum Request, stop sending data
if (mFirmwareUploadInProgress) {
mFirmwareUploadInProgress = false;
notifyLock();
@ -121,8 +124,10 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
default: {
/*
* If the DFU target device is in invalid state (e.g. the Init Packet is required but has not been selected), the target will send DFU_STATUS_INVALID_STATE error
* for each firmware packet that was send. We are interested may ignore all but the first one.
* If the DFU target device is in invalid state (e.g. the Init Packet is
* required but has not been selected), the target will send
* DFU_STATUS_INVALID_STATE error for each firmware packet that was send.
* We are interested may ignore all but the first one.
*/
if (mRemoteErrorOccurred)
break;
@ -142,12 +147,12 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
}
SecureDfuImpl(final Intent intent, final DfuBaseService service) {
SecureDfuImpl(@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(DFU_SERVICE_UUID);
if (dfuService == null)
return false;
@ -160,9 +165,14 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
@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,
@FileType final int fileType,
@NonNull final InputStream firmwareStream,
@Nullable final InputStream initPacketStream)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
if (initPacketStream == null) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, "The Init packet is required by this version DFU Bootloader");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR,
"The Init packet is required by this version DFU Bootloader");
mService.terminateConnection(gatt, DfuBaseService.ERROR_INIT_PACKET_REQUIRED);
return false;
}
@ -191,7 +201,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
@Override
public void performDfu(final Intent intent) throws DfuException, DeviceDisconnectedException, UploadAbortedException {
public void performDfu(@NonNull final Intent intent)
throws DfuException, DeviceDisconnectedException, UploadAbortedException {
logw("Secure DFU bootloader found");
mProgressInfo.setProgress(DfuBaseService.PROGRESS_STARTING);
@ -215,7 +226,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
try {
// Enable notifications
enableCCCD(mControlPointCharacteristic, NOTIFICATIONS);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Notifications enabled");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Notifications enabled");
// Wait a second here before going further
// Related:
@ -236,7 +248,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
mRemoteErrorOccurred = false;
logw("Sending SD+BL failed. Trying to send App only");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Invalid system components. Trying to send application");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
"Invalid system components. Trying to send application");
mFileType = DfuBaseService.TYPE_APPLICATION;
// Set new content type in the ZIP Input Stream and update sizes of images
@ -263,7 +276,7 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
// We are ready with DFU, the device is disconnected, let's close it and finalize the operation.
finalize(intent, false);
} catch (final UploadAbortedException e) {
} catch (@SuppressWarnings("CaughtExceptionImmediatelyRethrown") final UploadAbortedException e) {
// In secure DFU there is currently not possible to reset the device to application mode, so... do nothing
// The connection will be terminated in the DfuBaseService
throw e;
@ -275,14 +288,16 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
} catch (final RemoteDfuException e) {
final int error = DfuBaseService.ERROR_REMOTE_TYPE_SECURE | e.getErrorNumber();
loge(e.getMessage() + ": " + SecureDfuError.parse(error));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format(Locale.US, "Remote DFU error: %s", SecureDfuError.parse(error)));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, String.format(Locale.US,
"Remote DFU error: %s", SecureDfuError.parse(error)));
// For the Extended Error more details can be obtained on some devices.
if (e instanceof RemoteDfuExtendedErrorException) {
final RemoteDfuExtendedErrorException ee = (RemoteDfuExtendedErrorException) e;
final int extendedError = DfuBaseService.ERROR_REMOTE_TYPE_SECURE_EXTENDED | ee.getExtendedErrorNumber();
loge("Extended Error details: " + SecureDfuError.parseExtendedError(extendedError));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, "Details: " + SecureDfuError.parseExtendedError(extendedError) + " (Code = " + ee.getExtendedErrorNumber() + ")");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR,
"Details: " + SecureDfuError.parseExtendedError(extendedError) + " (Code = " + ee.getExtendedErrorNumber() + ")");
mService.terminateConnection(gatt, extendedError | DfuBaseService.ERROR_REMOTE_MASK);
} else {
mService.terminateConnection(gatt, error | DfuBaseService.ERROR_REMOTE_MASK);
@ -293,29 +308,38 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
/**
* This method does the following:
* <ol>
* <li>Selects the Command object - this Op Code returns the maximum acceptable size of a command object, and the offset and CRC32 of the command
* that is already stored in the device (in case the DFU was started in a previous connection and disconnected before it has finished).</li>
* <li>If the offset received is greater than 0 and less or equal to the size of the Init file that is to be sent, it will compare the
* <li>Selects the Command object - this Op Code returns the maximum acceptable size of a
* command object, and the offset and CRC32 of the command that is already stored in the
* device (in case the DFU was started in a previous connection and disconnected before
* it has finished).</li>
* <li>If the offset received is greater than 0 and less or equal to the size of the
* Init file that is to be sent, it will compare the
* received CRC with the local one and, if they match:
* <ul>
* <li>If offset < init file size - it will continue sending the Init file from the point it stopped before,</li>
* <li>If offset == init file size - it will send Execute command to execute the Init file, as it may have not been executed before.</li>
* <li>If offset < init file size - it will continue sending the Init file from the
* point it stopped before,</li>
* <li>If offset == init file size - it will send Execute command to execute the
* Init file, as it may have not been executed before.</li>
* </ul>
* </li>
* <li>If the CRC don't match, or the received offset is greater then init file size, it creates the Command Object and sends the whole
* Init file as the previous one was different.</li>
* <li>If the CRC don't match, or the received offset is greater then init file size,
* it creates the Command Object and sends the whole Init file as the previous one was
* different.</li>
* </ol>
* Sending of the Init packet is done without using PRNs (Packet Receipt Notifications), so they are disabled prior to sending the data.
* Sending of the Init packet is done without using PRNs (Packet Receipt Notifications),
* so they are disabled prior to sending the data.
*
* @param gatt the target GATT device
* @param allowResume true to allow resuming sending Init Packet. If false, it will be started again.
* @param gatt the target GATT device.
* @param allowResume true to allow resuming sending Init Packet. If false, it will be started
* again.
* @throws RemoteDfuException
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @throws UnknownResponseException
*/
private void sendInitPacket(final BluetoothGatt gatt, final boolean allowResume) throws RemoteDfuException, DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
private void sendInitPacket(@NonNull final BluetoothGatt gatt, final boolean allowResume)
throws RemoteDfuException, DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
final CRC32 crc32 = new CRC32(); // Used to calculate CRC32 of the Init packet
ObjectChecksum checksum;
@ -325,6 +349,7 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
final ObjectInfo info = selectObject(OBJECT_COMMAND);
logi(String.format(Locale.US, "Command object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Command object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
//noinspection StatementWithEmptyBody
if (mInitPacketSizeInBytes > info.maxSize) {
// Ignore this here. Later, after sending the 'Create object' command, DFU target will send an error if init packet is too large
}
@ -391,8 +416,13 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object created");
}
// Write Init data to the Packet Characteristic
logi("Sending " + (mInitPacketSizeInBytes - info.offset) + " bytes of init packet...");
writeInitData(mPacketCharacteristic, crc32);
try {
logi("Sending " + (mInitPacketSizeInBytes - info.offset) + " bytes of init packet...");
writeInitData(mPacketCharacteristic, crc32);
} catch (final DeviceDisconnectedException e) {
loge("Disconnected while sending init packet");
throw e;
}
final int crc = (int) (crc32.getValue() & 0xFFFFFFFFL);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Command object sent (CRC = %08X)", crc));
@ -442,33 +472,41 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
* This method does the following:
* <ol>
* <li>Sets the Packet Receipt Notification to a value specified in the settings.</li>
* <li>Selects the Data object - this returns maximum single object size and the offset and CRC of the data already saved.</li>
* <li>If the offset received is greater than 0 it will calculate the CRC of the same number of bytes of the firmware to be sent.
* If the CRC match it will continue sending data. Otherwise, it will go back to the beginning of the last chunk, or to the beginning
* of the previous chunk assuming the last one was not executed before, and continue sending data from there.</li>
* <li>If the CRC and offset received match and the offset is equal to the firmware size, it will only send the Execute command.</li>
* <li>Selects the Data object - this returns maximum single object size and the offset
* and CRC of the data already saved.</li>
* <li>If the offset received is greater than 0 it will calculate the CRC of the same
* number of bytes of the firmware to be sent. If the CRC match it will continue sending data.
* Otherwise, it will go back to the beginning of the last chunk, or to the beginning
* of the previous chunk assuming the last one was not executed before, and continue
* sending data from there.</li>
* <li>If the CRC and offset received match and the offset is equal to the firmware size,
* it will only send the Execute command.</li>
* </ol>
* @param gatt the target GATT device
* @param gatt the target GATT device.
* @throws RemoteDfuException
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @throws UnknownResponseException
*/
private void sendFirmware(final BluetoothGatt gatt) throws RemoteDfuException, DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
private void sendFirmware(final BluetoothGatt gatt) throws RemoteDfuException,
DeviceDisconnectedException, DfuException, UploadAbortedException, UnknownResponseException {
// Send the number of packets of firmware before receiving a receipt notification
int numberOfPacketsBeforeNotification = mPacketsBeforeNotification;
if (numberOfPacketsBeforeNotification > 0) {
setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
}
// We are ready to start sending the new firmware.
logi("Setting object to Data (Op Code = 6, Type = 2)");
final ObjectInfo info = selectObject(OBJECT_DATA);
logi(String.format(Locale.US, "Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
logi(String.format(Locale.US,
"Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US,
"Data object info received (Max size = %d, Offset = %d, CRC = %08X)", info.maxSize, info.offset, info.CRC32));
mProgressInfo.setMaxObjectSizeInBytes(info.maxSize);
// Number of chunks in which the data will be sent
@ -503,7 +541,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
if (crc == info.CRC32) {
logi(info.offset + " bytes of data sent before, CRC match");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, info.offset + " bytes of data sent before, CRC match");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
info.offset + " bytes of data sent before, CRC match");
mProgressInfo.setBytesSent(info.offset);
mProgressInfo.setBytesReceived(info.offset);
@ -517,7 +556,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
} else {
logi(info.offset + " bytes sent before, CRC does not match");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, info.offset + " bytes sent before, CRC does not match");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
info.offset + " bytes sent before, CRC does not match");
// The CRC of the current object is not correct. If there was another Data object sent before, its CRC must have been correct,
// as it has been executed. Either way, we have to create the current object again.
mProgressInfo.setBytesSent(bytesSentAndExecuted);
@ -526,7 +566,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
info.CRC32 = 0; // invalidate
mFirmwareStream.reset();
logi("Resuming from byte " + info.offset + "...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Resuming from byte " + info.offset + "...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Resuming from byte " + info.offset + "...");
}
} catch (final IOException e) {
loge("Error while reading firmware stream", e);
@ -549,10 +590,13 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
final int availableObjectSizeInBytes = mProgressInfo.getAvailableObjectSizeIsBytes();
logi("Creating Data object (Op Code = 1, Type = 2, Size = " + availableObjectSizeInBytes + ") (" + (currentChunk + 1) + "/" + chunkCount + ")");
writeCreateRequest(OBJECT_DATA, availableObjectSizeInBytes);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object (" + (currentChunk + 1) + "/" + chunkCount + ") created");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Uploading firmware...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Data object (" + (currentChunk + 1) + "/" + chunkCount + ") created");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Uploading firmware...");
} else {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Resuming uploading firmware...");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Resuming uploading firmware...");
resumeSendingData = false;
}
@ -569,13 +613,15 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
logi("Sending Calculate Checksum command (Op Code = 3)");
final ObjectChecksum checksum = readChecksum();
logi(String.format(Locale.US, "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US, "Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, String.format(Locale.US,
"Checksum received (Offset = %d, CRC = %08X)", checksum.offset, checksum.CRC32));
// It may happen, that not all bytes that were sent were received by the remote device
final int bytesLost = mProgressInfo.getBytesSent() - checksum.offset;
if (bytesLost > 0) {
logw(bytesLost + " bytes were lost!");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, bytesLost + " bytes were lost");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
bytesLost + " bytes were lost");
try {
// We have to reset the stream and read 'offset' number of bytes to recalculate the CRC
@ -590,7 +636,8 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
// To decrease the chance of loosing data next time let's set PRN to 1. This will make the update very long, but perhaps it will succeed.
numberOfPacketsBeforeNotification = mPacketsBeforeNotification = 1;
setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
"Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
}
// Calculate the CRC32
@ -673,9 +720,10 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
* Sets number of data packets that will be send before the notification will be received.
*
* @param data control point data packet
* @param value number of packets before receiving notification. If this value is 0, then the notification of packet receipt will be disabled by the DFU target.
* @param value number of packets before receiving notification. If this value is 0, then the
* notification of packet receipt will be disabled by the DFU target.
*/
private void setNumberOfPackets(final byte[] data, final int value) {
private void setNumberOfPackets(@SuppressWarnings("SameParameterValue") @NonNull final byte[] data, final int value) {
data[1] = (byte) (value & 0xFF);
data[2] = (byte) ((value >> 8) & 0xFF);
}
@ -686,7 +734,7 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
* @param data control point data packet
* @param value Object size in bytes.
*/
private void setObjectSize(final byte[] data, final int value) {
private void setObjectSize(@NonNull final byte[] data, final int value) {
data[2] = (byte) (value & 0xFF);
data[3] = (byte) ((value >> 8) & 0xFF);
data[4] = (byte) ((value >> 16) & 0xFF);
@ -694,20 +742,23 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
/**
* Sets the number of packets that needs to be sent to receive the Packet Receipt Notification. Value 0 disables PRNs.
* By default this is disabled. The PRNs may be used to send both the Data and Command object, but this Secure DFU implementation
* can handle them only during Data transfer.<br/>
* The intention of having PRNs is to make sure the outgoing BLE buffer is not getting overflown. The PRN will be sent after sending all
* packets from the queue.
* Sets the number of packets that needs to be sent to receive the Packet Receipt Notification.
* Value 0 disables PRNs. By default this is disabled. The PRNs may be used to send both the
* Data and Command object, but this Secure DFU implementation can handle them only during Data transfer.
* <p>
* The intention of having PRNs is to make sure the outgoing BLE buffer is not getting overflown.
* The PRN will be sent after sending all packets from the queue.
*
* @param number number of packets required before receiving a Packet Receipt Notification
* @param number number of packets required before receiving a Packet Receipt Notification.
* @throws DfuException
* @throws DeviceDisconnectedException
* @throws UploadAbortedException
* @throws UnknownResponseException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
*/
private void setPacketReceiptNotifications(final int number) throws DfuException, DeviceDisconnectedException, UploadAbortedException, UnknownResponseException, RemoteDfuException {
private void setPacketReceiptNotifications(final int number)
throws DfuException, DeviceDisconnectedException, UploadAbortedException,
UnknownResponseException, RemoteDfuException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");
@ -731,27 +782,31 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
* 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 characteristic the characteristic to write to. Should be the DFU CONTROL POINT.
* @param value the value to write to the characteristic.
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
*/
private void writeOpCode(final BluetoothGattCharacteristic characteristic, final byte[] value) throws DeviceDisconnectedException, DfuException, UploadAbortedException {
private void writeOpCode(@NonNull final BluetoothGattCharacteristic characteristic, @NonNull final byte[] value)
throws DeviceDisconnectedException, DfuException, UploadAbortedException {
writeOpCode(characteristic, value, false);
}
/**
* Writes Create Object request providing the type and size of the object.
* @param type {@link #OBJECT_COMMAND} or {@link #OBJECT_DATA}
* @param size size of the object or current part of the object
*
* @param type {@link #OBJECT_COMMAND} or {@link #OBJECT_DATA}.
* @param size size of the object or current part of the object.
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
* @throws UnknownResponseException
*/
private void writeCreateRequest(final int type, final int size) throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException {
private void writeCreateRequest(final int type, final int size)
throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException,
UnknownResponseException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to create object: device disconnected");
@ -768,15 +823,19 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
/**
* Selects the current object and reads its metadata. The object info contains the max object size, and the offset and CRC32 of the whole object until now.
* Selects the current object and reads its metadata. The object info contains the max object
* size, and the offset and CRC32 of the whole object until now.
*
* @return object info
* @return object info.
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
* @throws RemoteDfuException thrown when the returned status code is not equal to
* {@link #DFU_STATUS_SUCCESS}.
*/
private ObjectInfo selectObject(final int type) throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException {
private ObjectInfo selectObject(final int type)
throws DeviceDisconnectedException, DfuException, UploadAbortedException,
RemoteDfuException, UnknownResponseException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read object info: device disconnected");
@ -798,15 +857,18 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
/**
* Sends the Calculate Checksum request. As a response a notification will be sent with current offset and CRC32 of the current object.
* Sends the Calculate Checksum request. As a response a notification will be sent with current
* offset and CRC32 of the current object.
*
* @return requested object info
* @return requested object info.
* @throws DeviceDisconnectedException
* @throws DfuException
* @throws UploadAbortedException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
* @throws RemoteDfuException thrown when the returned status code is not equal to
* {@link #DFU_STATUS_SUCCESS}.
*/
private ObjectChecksum readChecksum() throws DeviceDisconnectedException, DfuException, UploadAbortedException, RemoteDfuException, UnknownResponseException {
private ObjectChecksum readChecksum() throws DeviceDisconnectedException, DfuException,
UploadAbortedException, RemoteDfuException, UnknownResponseException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");
@ -828,15 +890,18 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
/**
* Sends the Execute operation code and awaits for a return notification containing status code.
* The Execute command will confirm the last chunk of data or the last command that was sent.
* Creating the same object again, instead of executing it allows to retransmitting it in case of a CRC error.
* Creating the same object again, instead of executing it allows to retransmitting it in case
* of a CRC error.
*
* @throws DfuException
* @throws DeviceDisconnectedException
* @throws UploadAbortedException
* @throws UnknownResponseException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
* @throws RemoteDfuException thrown when the returned status code is not equal to
* {@link #DFU_STATUS_SUCCESS}.
*/
private void writeExecute() throws DfuException, DeviceDisconnectedException, UploadAbortedException, UnknownResponseException, RemoteDfuException {
private void writeExecute() throws DfuException, DeviceDisconnectedException,
UploadAbortedException, UnknownResponseException, RemoteDfuException {
if (!mConnected)
throw new DeviceDisconnectedException("Unable to read Checksum: device disconnected");
@ -861,14 +926,19 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
* so such update is forbidden as it would brick the device.</li>
* </ol>
* See: <a href="https://github.com/NordicSemiconductor/Android-DFU-Library/issues/86">https://github.com/NordicSemiconductor/Android-DFU-Library/issues/86</a>
* @param allowRetry if true service will retry to send the execute command in case of INVALID_OBJECT error.
*
* @param allowRetry if true service will retry to send the execute command in case of
* INVALID_OBJECT error.
* @throws DfuException
* @throws DeviceDisconnectedException
* @throws UploadAbortedException
* @throws UnknownResponseException
* @throws RemoteDfuException thrown when the returned status code is not equal to {@link #DFU_STATUS_SUCCESS}
* @throws RemoteDfuException thrown when the returned status code is not equal to
* {@link #DFU_STATUS_SUCCESS}.
*/
private void writeExecute(final boolean allowRetry) throws DfuException, DeviceDisconnectedException, UploadAbortedException, UnknownResponseException, RemoteDfuException {
private void writeExecute(final boolean allowRetry)
throws DfuException, DeviceDisconnectedException, UploadAbortedException,
UnknownResponseException, RemoteDfuException {
try {
writeExecute();
} catch (final RemoteDfuException e) {
@ -890,11 +960,11 @@ class SecureDfuImpl extends BaseCustomDfuImpl {
}
private class ObjectInfo extends ObjectChecksum {
protected int maxSize;
int maxSize;
}
private class ObjectChecksum {
protected int offset;
protected int CRC32;
int offset;
int CRC32;
}
}

View File

@ -26,9 +26,11 @@ import android.content.Intent;
import android.os.ParcelUuid;
import android.os.Parcelable;
import androidx.annotation.NonNull;
/* package */ class UuidHelper {
/* package */ static void assignCustomUuids(final Intent intent) {
/* package */ static void assignCustomUuids(@NonNull final Intent intent) {
// Added in SDK 4.3.0. Legacy DFU and Legacy bootloader share the same UUIDs.
Parcelable[] uuids = intent.getParcelableArrayExtra(DfuBaseService.EXTRA_CUSTOM_UUIDS_FOR_LEGACY_DFU);
if (uuids != null && uuids.length == 4) {

View File

@ -22,7 +22,6 @@
package no.nordicsemi.android.dfu.internal;
import androidx.annotation.NonNull;
import android.util.Log;
import com.google.gson.Gson;
@ -38,6 +37,7 @@ import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import androidx.annotation.NonNull;
import no.nordicsemi.android.dfu.DfuBaseService;
import no.nordicsemi.android.dfu.internal.manifest.FileInfo;
import no.nordicsemi.android.dfu.internal.manifest.Manifest;
@ -45,15 +45,22 @@ import no.nordicsemi.android.dfu.internal.manifest.ManifestFile;
import no.nordicsemi.android.dfu.internal.manifest.SoftDeviceBootloaderFileInfo;
/**
* <p>Reads the firmware files from the a ZIP file. The ZIP file must be either created using the <b>nrf utility</b> tool, available together with Master Control Panel v3.8.0+,
* or follow the backward compatibility syntax: must contain only files with names: application.hex/bin, softdevice.hex/dat or bootloader.hex/bin, optionally also application.dat
* and/or system.dat with init packets.</p>
* <p>The ArchiveInputStream will read only files with types specified by <b>types</b> parameter of the constructor.</p>
* <p>
* Reads the firmware files from the a ZIP file. The ZIP file must be either created using the
* <a href="https://github.com/NordicSemiconductor/pc-nrfutil"><b>nrf util</b></a>,
* or follow the backward compatibility syntax: must contain only files with names:
* application.hex/bin, softdevice.hex/dat or bootloader.hex/bin, optionally also application.dat
* and/or system.dat with init packets.
* <p>
* The ArchiveInputStream will read only files with types specified by <b>types</b> parameter of
* the constructor.
*/
public class ArchiveInputStream extends InputStream {
private static final String TAG = "DfuArchiveInputStream";
/** The name of the manifest file is fixed. */
/**
* The name of the manifest file is fixed.
*/
private static final String MANIFEST = "manifest.json";
// Those file names are for backwards compatibility mode
private static final String SOFTDEVICE_HEX = "softdevice.hex";
@ -67,7 +74,10 @@ public class ArchiveInputStream extends InputStream {
private final ZipInputStream zipInputStream;
/** Contains bytes arrays with BIN files. HEX files are converted to BIN before being added to this map. */
/**
* Contains bytes arrays with BIN files. HEX files are converted to BIN before being
* added to this map.
*/
private Map<String, byte[]> entries;
private Manifest manifest;
private CRC32 crc32;
@ -91,12 +101,13 @@ public class ArchiveInputStream extends InputStream {
/**
* <p>
* The ArchiveInputStream read HEX or BIN files from the Zip stream. It may skip some of them, depending on the value of the types parameter.
* This is useful if the DFU service wants to send the Soft Device and Bootloader only, and then the Application in the following connection, despite
* the ZIP file contains all 3 HEX/BIN files.
* The ArchiveInputStream read HEX or BIN files from the Zip stream. It may skip some of them,
* depending on the value of the types parameter. This is useful if the DFU service wants to
* send the Soft Device and Bootloader only, and then the Application in the following connection,
* despite the ZIP file contains all 3 HEX/BIN files.
* When types is equal to {@link DfuBaseService#TYPE_AUTO} all present files are read.
* </p>
* <p>Use bit combination of the following types:</p>
* <p>
* Use bit combination of the following types:
* <ul>
* <li>{@link DfuBaseService#TYPE_SOFT_DEVICE}</li>
* <li>{@link DfuBaseService#TYPE_BOOTLOADER}</li>
@ -104,15 +115,15 @@ public class ArchiveInputStream extends InputStream {
* <li>{@link DfuBaseService#TYPE_AUTO}</li>
* </ul>
*
* @param stream
* the Zip Input Stream
* @param mbrSize
* The size of the MRB segment (Master Boot Record) on the device. The parser will cut data from addresses below that number from all HEX files.
* @param types
* File types that are to be read from the ZIP. Use {@link DfuBaseService#TYPE_APPLICATION} etc.
* @throws java.io.IOException
* @param stream the Zip Input Stream
* @param mbrSize The size of the MRB segment (Master Boot Record) on the device.
* The parser will cut data from addresses below that number from all HEX files.
* @param types File types that are to be read from the ZIP. Use
* {@link DfuBaseService#TYPE_APPLICATION} etc.
* @throws java.io.IOException Thrown in case of an invalid ZIP file.
*/
public ArchiveInputStream(final InputStream stream, final int mbrSize, final int types) throws IOException {
public ArchiveInputStream(final InputStream stream, final int mbrSize, final int types)
throws IOException {
this.zipInputStream = new ZipInputStream(stream);
this.crc32 = new CRC32();
@ -262,12 +273,15 @@ public class ArchiveInputStream extends InputStream {
/**
* Reads all files into byte arrays.
* Here we don't know whether the ZIP file is valid.
*
* The ZIP file is valid when contains a 'manifest.json' file and all BIN and DAT files that are specified in the manifest.
*
* For backwards compatibility ArchiveInputStream supports also ZIP archives without 'manifest.json' file
* but than it MUST include at least one of the following files: softdevice.bin/hex, bootloader.bin/hex, application.bin/hex.
* To support the init packet such ZIP file should contain also application.dat and/or system.dat (with the CRC16 of a SD, BL or SD+BL together).
* <p>
* The ZIP file is valid when contains a 'manifest.json' file and all BIN and DAT files that
* are specified in the manifest.
* <p>
* For backwards compatibility ArchiveInputStream supports also ZIP archives without
* 'manifest.json' file but than it MUST include at least one of the following files:
* softdevice.bin/hex, bootloader.bin/hex, application.bin/hex.
* To support the init packet such ZIP file should contain also application.dat and/or system.dat
* (with the CRC16 of a SD, BL or SD+BL together).
*/
private void parseZip(final int mbrSize) throws IOException {
final byte[] buffer = new byte[1024];
@ -307,7 +321,8 @@ public class ArchiveInputStream extends InputStream {
// Some validation
if (entries.isEmpty()) {
throw new FileNotFoundException("No files found in the ZIP. Check if the URI provided is valid and the ZIP contains required files on root level, not in a directory.");
throw new FileNotFoundException("No files found in the ZIP. Check if the URI provided is " +
"valid and the ZIP contains required files on root level, not in a directory.");
}
if (manifestData != null) {
@ -319,7 +334,8 @@ public class ArchiveInputStream extends InputStream {
"to your proguard rules?");
}
} else {
Log.w(TAG, "Manifest not found in the ZIP. It is recommended to use a distribution file created with: https://github.com/NordicSemiconductor/pc-nrfutil/ (for Legacy DFU use version 0.5.x)");
Log.w(TAG, "Manifest not found in the ZIP. It is recommended to use a distribution " +
"file created with: https://github.com/NordicSemiconductor/pc-nrfutil/ (for Legacy DFU use version 0.5.x)");
}
}
@ -327,7 +343,7 @@ public class ArchiveInputStream extends InputStream {
public void close() throws IOException {
softDeviceBytes = null;
bootloaderBytes = null;
softDeviceBytes = null;
applicationBytes = null;
softDeviceAndBootloaderBytes = null;
softDeviceSize = bootloaderSize = applicationSize = 0;
currentSource = null;
@ -381,6 +397,7 @@ public class ArchiveInputStream extends InputStream {
/**
* Marks the current position in the stream. The parameter is ignored.
*
* @param readlimit this parameter is ignored, can be anything
*/
@Override
@ -429,6 +446,7 @@ public class ArchiveInputStream extends InputStream {
/**
* Returns the CRC32 of the part of the firmware that was already read.
*
* @return the CRC
*/
public long getCrc32() {
@ -436,14 +454,17 @@ public class ArchiveInputStream extends InputStream {
}
/**
* Returns the content type based on the content of the ZIP file. The content type may be truncated using {@link #setContentType(int)}.
* Returns the content type based on the content of the ZIP file. The content type may be
* truncated using {@link #setContentType(int)}.
*
* @return a bit field of {@link DfuBaseService#TYPE_SOFT_DEVICE TYPE_SOFT_DEVICE}, {@link DfuBaseService#TYPE_BOOTLOADER TYPE_BOOTLOADER} and {@link DfuBaseService#TYPE_APPLICATION
* TYPE_APPLICATION}
* @return A bit field of {@link DfuBaseService#TYPE_SOFT_DEVICE TYPE_SOFT_DEVICE},
* {@link DfuBaseService#TYPE_BOOTLOADER TYPE_BOOTLOADER} and
* {@link DfuBaseService#TYPE_APPLICATION TYPE_APPLICATION}
*/
public int getContentType() {
type = 0;
// In Secure DFU the softDeviceSize and bootloaderSize may be 0 if both are in the ZIP file. The size of each part is embedded in the Init packet.
// In Secure DFU the softDeviceSize and bootloaderSize may be 0 if both are in the ZIP file.
// The size of each part is embedded in the Init packet.
if (softDeviceAndBootloaderBytes != null)
type |= DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER;
// In Legacy DFU the size of each of these parts was given in the manifest file.
@ -457,11 +478,11 @@ public class ArchiveInputStream extends InputStream {
}
/**
* Truncates the current content type. May be used to hide some files, e.g. to send Soft Device and Bootloader without Application or only the Application.
* Truncates the current content type. May be used to hide some files, e.g. to send Soft Device
* and Bootloader without Application or only the Application.
*
* @param type
* the new type
* @return the final type after truncating
* @param type the new type.
* @return The final type after truncating.
*/
public int setContentType(final int type) {
this.type = type;
@ -497,9 +518,10 @@ public class ArchiveInputStream extends InputStream {
}
/**
* Sets the currentSource to the new file or to <code>null</code> if the last file has been transmitted.
* Sets the currentSource to the new file or to <code>null</code> if the last file has been
* transmitted.
*
* @return the new source, the same as {@link #currentSource}
* @return The new source, the same as {@link #currentSource}.
*/
private byte[] startNextFile() {
byte[] ret;
@ -515,12 +537,15 @@ public class ArchiveInputStream extends InputStream {
}
/**
* Returns the number of bytes that has not been read yet. This value includes only firmwares matching the content type set by the construcotor or the {@link #setContentType(int)} method.
* Returns the number of bytes that has not been read yet. This value includes only
* firmwares matching the content type set by the constructor or the
* {@link #setContentType(int)} method.
*/
@Override
public int available() {
// In Secure DFU softdevice and bootloader sizes are not provided in the Init file (they are encoded inside the Init file instead).
// The service doesn't send those sizes, not the whole size of the firmware separately, like it was done in the Legacy DFU.
// In Secure DFU softdevice and bootloader sizes are not provided in the Init file
// (they are encoded inside the Init file instead). The service doesn't send those sizes,
// not the whole size of the firmware separately, like it was done in the Legacy DFU.
// This method then is just used to log file size.
// In case of SD+BL in Secure DFU:
@ -533,42 +558,54 @@ public class ArchiveInputStream extends InputStream {
}
/**
* Returns the total size of the SoftDevice firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the SoftDevice firmware (BIN part)
* Returns the total size of the SoftDevice firmware. In case the firmware was given as a HEX,
* this method returns the size of the BIN content of the file.
*
* @return The size of the SoftDevice firmware (BIN part).
*/
public int softDeviceImageSize() {
return (type & DfuBaseService.TYPE_SOFT_DEVICE) > 0 ? softDeviceSize : 0;
}
/**
* Returns the total size of the Bootloader firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the Bootloader firmware (BIN part)
* Returns the total size of the Bootloader firmware. In case the firmware was given as a HEX,
* this method returns the size of the BIN content of the file.
*
* @return The size of the Bootloader firmware (BIN part).
*/
public int bootloaderImageSize() {
return (type & DfuBaseService.TYPE_BOOTLOADER) > 0 ? bootloaderSize : 0;
}
/**
* Returns the total size of the Application firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the Application firmware (BIN part)
* Returns the total size of the Application firmware. In case the firmware was given as a HEX,
* this method returns the size of the BIN content of the file.
*
* @return The size of the Application firmware (BIN part).
*/
public int applicationImageSize() {
return (type & DfuBaseService.TYPE_APPLICATION) > 0 ? applicationSize : 0;
}
/**
* Returns the content of the init file for SoftDevice and/or Bootloader. When both SoftDevice and Bootloader are present in the ZIP file (as two files using the compatibility mode
* or as one file using the new Distribution packet) the system init contains validation data for those two files combined (e.g. the CRC value). This method may return
* <code>null</code> if there is no SoftDevice nor Bootloader in the ZIP or the DAT file is not present there.
* @return the content of the init packet for SoftDevice and/or Bootloader
* Returns the content of the init file for SoftDevice and/or Bootloader. When both SoftDevice
* and Bootloader are present in the ZIP file (as two files using the compatibility mode
* or as one file using the new Distribution packet) the system init contains validation data
* for those two files combined (e.g. the CRC value). This method may return
* <code>null</code> if there is no SoftDevice nor Bootloader in the ZIP or the DAT file is
* not present there.
*
* @return The content of the init packet for SoftDevice and/or Bootloader.
*/
public byte[] getSystemInit() {
return systemInitBytes;
}
/**
* Returns the content of the init file for the Application or <code>null</code> if no application file in the ZIP, or the DAT file is not provided.
* @return the content of the init packet for Application
* Returns the content of the init file for the Application or <code>null</code> if no
* application file in the ZIP, or the DAT file is not provided.
*
* @return The content of the init packet for Application.
*/
public byte[] getApplicationInit() {
return applicationInitBytes;
@ -576,9 +613,12 @@ public class ArchiveInputStream extends InputStream {
/**
* This method returns true if the content of the ZIP file may be sent only using Secure DFU.
* The reason may be that the ZIP contains a single bin file with SD and/or BL together with App, which has to be sent in a single connection.
* Sizes of each component are not given explicitly in the Manifest (even if they are, they are ignored). They are hidden in the Init Packet instead.
* @return true if the content of this ZIP may only be sent using Secure DFU.
* The reason may be that the ZIP contains a single bin file with SD and/or BL together with
* App, which has to be sent in a single connection.
* Sizes of each component are not given explicitly in the Manifest (even if they are,
* they are ignored). They are hidden in the Init Packet instead.
*
* @return True if the content of this ZIP may only be sent using Secure DFU.
*/
public boolean isSecureDfuRequired() {
return manifest != null && manifest.isSecureDfuRequired();

View File

@ -24,6 +24,7 @@ package no.nordicsemi.android.dfu.internal.manifest;
import com.google.gson.annotations.SerializedName;
@SuppressWarnings("unused")
public class Manifest {
private FileInfo application;
private FileInfo bootloader;

View File

@ -22,6 +22,7 @@
package no.nordicsemi.android.dfu.internal.manifest;
@SuppressWarnings("unused")
public class ManifestFile {
private Manifest manifest;

View File

@ -1,6 +1,6 @@
#Mon Oct 01 13:36:04 CEST 2018
#Wed Feb 20 13:14:30 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip