Java tutorial
/******************************************************************************* * Copyright (c) 2013 Nordic Semiconductor. All Rights Reserved. * * The information contained herein is property of Nordic Semiconductor ASA. * Terms and conditions of usage are described in detail in NORDIC SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. * Licensees are granted free, non-transferable use of the information. NO WARRANTY of ANY KIND is provided. * This heading must NOT be removed from the file. ******************************************************************************/ /* * NORDIC SEMICONDUTOR EXAMPLE CODE AND LICENSE AGREEMENT * * You are receiving this document because you have obtained example code ("Software") * from Nordic Semiconductor ASA * ("Licensor"). The Software is protected by copyright * laws and international treaties. All intellectual property rights related to the * Software is the property of the Licensor. This document is a license agreement governing * your rights and obligations regarding usage of the Software. Any variation to the terms * of this Agreement shall only be valid if made in writing by the Licensor. * * == Scope of license rights == * * You are hereby granted a limited, non-exclusive, perpetual right to use and modify the * Software in order to create your own software. You are entitled to distribute the * Software in original or modified form as part of your own software. * * If distributing your software in source code form, a copy of this license document shall * follow with the distribution. * * The Licensor can at any time terminate your rights under this license agreement. * * == Restrictions on license rights == * * You are not allowed to distribute the Software on its own, without incorporating it into * your own software. * * You are not allowed to remove, alter or destroy any proprietary, * trademark or copyright markings or notices placed upon or contained with the Software. * * You shall not use Licensor's name or trademarks without Licensor's prior consent. * * == Disclaimer of warranties and limitation of liability == * * YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THE SOFTWARE IS AT YOUR OWN RISK AND THAT THE * SOFTWARE IS PROVIDED *AS IS" WITHOUT ANY WARRANTIES OR CONDITIONS WHATSOEVER. NORDIC SEMICONDUCTOR ASA * DOES NOT WARRANT THAT THE FUNCTIONS OF THE SOFTWARE WILL MEET YOUR REQUIREMENTS OR THAT THE * OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR FREE. YOU ASSUME RESPONSIBILITY FOR * SELECTING THE SOFTWARE TO ACHIEVE YOUR INTENDED RESULTS, AND FOR THE *USE AND THE RESULTS * OBTAINED FROM THE SOFTWARE. * * NORDIC SEMICONDUCTOR ASA DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED * TO WARRANTIES RELATED TO: NON-INFRINGEMENT, LACK OF VIRUSES, ACCURACY OR COMPLETENESS OF RESPONSES * OR RESULTS, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL OR * CONSEQUENTIAL DAMAGES OR FOR ANY DAMAGES WHATSOEVER (INCLUDING BUT NOT LIMITED TO DAMAGES FOR * LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, PERSONAL INJURY, * LOSS OF PRIVACY OR OTHER PECUNIARY OR OTHER LOSS WHATSOEVER) ARISING OUT OF USE OR INABILITY TO * USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. * * REGARDLESS OF THE FORM OF ACTION, NORDIC SEMICONDUCTOR ASA AGGREGATE LIABILITY ARISING OUT OF * OR RELATED TO THIS AGREEMENT SHALL NOT EXCEED THE TOTAL AMOUNT PAYABLE BY YOU UNDER THIS AGREEMENT. * THE FOREGOING LIMITATIONS, EXCLUSIONS AND DISCLAIMERS SHALL APPLY TO THE MAXIMUM EXTENT ALLOWED BY * APPLICABLE LAW. * * == Dispute resolution and legal venue == * * Any and all disputes arising out of the rights and obligations in this license agreement shall be * submitted to ordinary court proceedings. You accept the Oslo City Court as legal venue under this agreement. * * This license agreement shall be governed by Norwegian law. * * == Contact information == * * All requests regarding the Software or the API shall be directed to: * Nordic Semiconductor ASA, P.O. Box 436, Skyen, 0213 Oslo, Norway. * * http://www.nordicsemi.com/eng/About-us/Contact-us */ package no.nordicsemi.android.nrftoolbox.dfu; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.UUID; import org.apache.http.client.methods.HttpGet; import com.mbientlab.metawear.api.characteristic.MetaWear; import com.mbientlab.metawear.api.GATT.GATTService; import com.mbientlab.metawear.api.controller.Debug; import com.mbientlab.metawear.api.util.Registers; import com.mbientlab.metawear.app.R; import com.mbientlab.metawear.app.BuildConfig; import no.nordicsemi.android.log.LogContract.Log.Level; import no.nordicsemi.android.log.LogSession; import no.nordicsemi.android.log.Logger; import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsFragment; import no.nordicsemi.android.nrftoolbox.utility.GattError; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; public class DfuService extends IntentService { private static final String TAG = "DfuService"; public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.extra.dfu.EXTRA_DEVICE_ADDRESS"; public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.extra.dfu.EXTRA_DEVICE_NAME"; public static final String EXTRA_LOG_URI = "no.nordicsemi.android.extra.dfu.EXTRA_LOG_URI"; public static final String EXTRA_FILE_PATH = "no.nordicsemi.android.extra.dfu.EXTRA_FILE_PATH"; public static final String EXTRA_FILE_URI = "no.nordicsemi.android.extra.dfu.EXTRA_FILE_URI"; public static final String EXTRA_DATA = "no.nordicsemi.android.extra.dfu.EXTRA_DATA"; public static final String PREFS_DFU_IN_PROGRESS = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_DFU_IN_PROGRESS"; public static final String BROADCAST_ERROR = "no.nordicsemi.android.broadcast.dfu.BROADCAST_ERROR"; public static final int ERROR_MASK = 0x0100; public static final int ERROR_DEVICE_DISCONNECTED = ERROR_MASK | 0x00; public static final int ERROR_FILE_NOT_FOUND = ERROR_MASK | 0x01; public static final int ERROR_FILE_CLOSED = ERROR_MASK | 0x02; public static final int ERROR_FILE_INVALID = ERROR_MASK | 0x03; public static final int ERROR_FILE_IO_EXCEPTION = ERROR_MASK | 0x04; public static final int ERROR_SERVICE_DISCOVERY_NOT_STARTED = ERROR_MASK | 0x05; public static final int ERROR_SERVICE_NOT_FOUND = ERROR_MASK | 0x06; public static final int ERROR_CHARACTERISTICS_NOT_FOUND = ERROR_MASK | 0x07; public static final int ERROR_INVALID_RESPONSE = ERROR_MASK | 0x08; /** Look for DFU specification to get error codes */ public static final int ERROR_REMOTE_MASK = 0x0200; public static final int ERROR_CONNECTION_MASK = 0x0400; public static final String BROADCAST_PROGRESS = "no.nordicsemi.android.broadcast.dfu.BROADCAST_PROGRESS"; public static final int PROGRESS_CONNECTING = -1; public static final int PROGRESS_STARTING = -2; public static final int PROGRESS_VALIDATING = -4; public static final int PROGRESS_DISCONNECTING = -5; public static final int PROGRESS_COMPLETED = -6; public static final int PROGRESS_ABORTED = -7; /** The log events are only broadcasted when there is no LogView application installed */ public static final String BROADCAST_LOG = "no.nordicsemi.android.broadcast.dfu.BROADCAST_LOG"; public static final String EXTRA_LOG_MESSAGE = "no.nordicsemi.android.extra.dfu.EXTRA_LOG_INFO"; public static final String EXTRA_LOG_LEVEL = "no.nordicsemi.android.extra.dfu.EXTRA_LOG_LEVEL"; /** Activity may broadcast this broadcast in order to pause, resume or abort DFU process */ public static final String BROADCAST_ACTION = "no.nordicsemi.android.broadcast.dfu.BROADCAST_ACTION"; public static final String EXTRA_ACTION = "no.nordicsemi.android.extra.dfu.EXTRA_ACTION"; public static final int ACTION_PAUSE = 0; public static final int ACTION_RESUME = 1; public static final int ACTION_ABORT = 2; public static final int NOTIFICATION_ID = 283; // a random number private BluetoothAdapter mBluetoothAdapter; private String mDeviceAddress; private String mDeviceName; private LogSession mLogSession; /** Lock used in synchronization purposes */ private final Object mLock = new Object(); /** The number of the last error that has occurred or 0 if there was no error */ private int mErrorState; /** The current connection state. If its value is > 0 than an error has occurred. Error number is a negative value of mConnectionState */ private int mConnectionState; private final static int STATE_DISCONNECTED = 0; private final static int STATE_CONNECTING = -1; private final static int STATE_CONNECTED = -2; private final static int STATE_CONNECTED_AND_READY = -3; // indicates that services were discovered private final static int STATE_DISCONNECTING = -4; private final static int STATE_CLOSED = -5; /** Flag set when we got confirmation from the device that notifications are enabled. */ private boolean mNotificationsEnabled; private final static int MAX_PACKET_SIZE = 20; // the maximum number of bytes in one packet is 20. May be less. /** The number of packets of firmware data to be send before receiving a new Packets receipt notification. 0 disables the packets notifications */ private int mPacketsBeforeNotification = 10; private byte[] mBuffer = new byte[MAX_PACKET_SIZE]; private HexInputStream mHexInputStream; private int mImageSizeInBytes; private int mImageSizeInPackets; private int mBytesSent; private int mBytesConfirmed; private int mPacketsSendSinceNotification; private boolean mPaused; private boolean mAborted; /** Flag indicating whether the image size has been already transfered or not */ private boolean mImageSizeSent; /** Flag indicating whether the request was completed or not */ private boolean mRequestCompleted; /** Latest data received from device using notification. */ private byte[] mReceivedData = null; private static final int OP_CODE_RECEIVE_START_DFU_KEY = 0x01; // 1 private static final int OP_CODE_RECEIVE_FIRMWARE_IMAGE_KEY = 0x03; // 3 private static final int OP_CODE_RECEIVE_VALIDATE_KEY = 0x04; // 4 private static final int OP_CODE_RECEIVE_ACTIVATE_AND_RESET_KEY = 0x05; // 5 private static final int OP_CODE_RECEIVE_RESET_KEY = 0x06; // 6 // private static final int OP_CODE_PACKET_REPORT_RECEIVED_IMAGE_SIZE_KEY = 0x07; // 7 private static final int OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY = 0x08; // 8 private static final int OP_CODE_RESPONSE_CODE_KEY = 0x10; // 16 private static final int OP_CODE_PACKET_RECEIPT_NOTIF_KEY = 0x11; // 11 private static final byte[] OP_CODE_START_DFU = new byte[] { OP_CODE_RECEIVE_START_DFU_KEY }; private static final byte[] OP_CODE_RECEIVE_FIRMWARE_IMAGE = new byte[] { OP_CODE_RECEIVE_FIRMWARE_IMAGE_KEY }; private static final byte[] OP_CODE_VALIDATE = new byte[] { OP_CODE_RECEIVE_VALIDATE_KEY }; private static final byte[] OP_CODE_ACTIVATE_AND_RESET = new byte[] { OP_CODE_RECEIVE_ACTIVATE_AND_RESET_KEY }; private static final byte[] OP_CODE_RESET = new byte[] { OP_CODE_RECEIVE_RESET_KEY }; // private static final byte[] OP_CODE_REPORT_RECEIVED_IMAGE_SIZE = new byte[] { OP_CODE_PACKET_REPORT_RECEIVED_IMAGE_SIZE_KEY }; private static final byte[] OP_CODE_PACKET_RECEIPT_NOTIF_REQ = new byte[] { OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY, 0x00, 0x00 }; public static final int DFU_STATUS_SUCCESS = 1; public static final int DFU_STATUS_INVALID_STATE = 2; public static final int DFU_STATUS_NOT_SUPPORTED = 3; public static final int DFU_STATUS_DATA_SIZE_EXCEEDS_LIMIT = 4; public static final int DFU_STATUS_CRC_ERROR = 5; public static final int DFU_STATUS_OPERATION_FAILED = 6; public static final UUID DFU_SERVICE_UUID = new UUID(0x000015301212EFDEl, 0x1523785FEABCD123l); private static final UUID DFU_CONTROL_POINT_UUID = new UUID(0x000015311212EFDEl, 0x1523785FEABCD123l); private static final UUID DFU_PACKET_UUID = new UUID(0x000015321212EFDEl, 0x1523785FEABCD123l); private static final UUID CLIENT_CHARACTERISTIC_CONFIG = new UUID(0x0000290200001000l, 0x800000805f9b34fbl); private final BroadcastReceiver mDfuActionReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final int action = intent.getIntExtra(EXTRA_ACTION, 0); switch (action) { case ACTION_PAUSE: mPaused = true; break; case ACTION_RESUME: mPaused = false; // notify waiting thread synchronized (mLock) { mLock.notifyAll(); } break; case ACTION_ABORT: mPaused = false; mAborted = true; // notify waiting thread synchronized (mLock) { mLock.notifyAll(); } break; } } }; public static String ACTION_RECONNECT_BOOTLOADER = "ACTION_RECONNECT_BOOTLOADER"; private BluetoothGattCallback normalCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { switch (newState) { case BluetoothProfile.STATE_CONNECTED: gatt.discoverServices(); break; case BluetoothProfile.STATE_DISCONNECTED: mConnectionState = STATE_DISCONNECTED; synchronized (mLock) { mLock.notifyAll(); } break; } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { BluetoothGattService mwBleService = gatt.getService(GATTService.METAWEAR.uuid()); BluetoothGattCharacteristic cmdRegister = mwBleService.getCharacteristic(MetaWear.COMMAND.uuid()); cmdRegister.setValue(Registers.buildWriteCommand(Debug.Register.JUMP_TO_BOOTLOADER)); gatt.writeCharacteristic(cmdRegister); } }; private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) { // check whether an error occurred if (status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothGatt.STATE_CONNECTED) { logi("Connected to GATT server"); mConnectionState = STATE_CONNECTED; // Attempts to discover services after successful connection. // do not refresh the gatt device here! final boolean success = gatt.discoverServices(); logi("Attempting to start service discovery... " + (success ? "succeed" : "failed")); if (!success) { mErrorState = ERROR_SERVICE_DISCOVERY_NOT_STARTED; } else { // just return here, lock will be notified when service discovery finishes return; } } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { logi("Disconnected from GATT server"); mConnectionState = STATE_DISCONNECTED; } } else { loge("Connection state change error: " + status + " newState: " + newState); mErrorState = ERROR_CONNECTION_MASK | status; } // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } } @Override public void onServicesDiscovered(final BluetoothGatt gatt, final int status) { if (status == BluetoothGatt.GATT_SUCCESS) { logi("Services discovered"); mConnectionState = STATE_CONNECTED_AND_READY; } else { loge("Service discovery error: " + status); mErrorState = ERROR_CONNECTION_MASK | status; } // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } } @Override public void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (CLIENT_CHARACTERISTIC_CONFIG.equals(descriptor.getUuid())) { // we have enabled or disabled characteristic mNotificationsEnabled = descriptor.getValue()[0] == 1; } } else { loge("Descriptor write error: " + status); mErrorState = ERROR_CONNECTION_MASK | status; } // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } }; @Override public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { 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 flag to true. * If the PACKET characteristic was written we must: * - if the image size was written in DFU Start procedure, just set flag to true * - else * - send the next packet, if notification is not required at that moment * - do nothing, because we have to wait for the notification to confirm the data received */ if (DFU_PACKET_UUID.equals(characteristic.getUuid())) { if (mImageSizeSent) { // if the PACKET characteristic was written with image data, update counters mBytesSent += characteristic.getValue().length; mPacketsSendSinceNotification++; // if a packet receipt notification is expected, or the last packet was sent, do nothing. There onCharacteristicChanged listener will catch either // a packet confirmation (if there are more bytes to send) or the image received notification (it upload process was completed) final boolean notificationExpected = mPacketsBeforeNotification > 0 && mPacketsSendSinceNotification == mPacketsBeforeNotification; final boolean lastPacketTransfered = mBytesSent == mImageSizeInBytes; if (notificationExpected || lastPacketTransfered) return; // when neither of them is true, send the next packet try { waitIfPaused(); if (mAborted) { // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } } final byte[] buffer = mBuffer; final int size = mHexInputStream.readPacket(buffer); writePacket(gatt, characteristic, buffer, size); updateProgressNotification(); return; } catch (final HexFileValidationException e) { loge("Invalid HEX file"); mErrorState = ERROR_FILE_INVALID; } catch (final IOException e) { loge("Error while reading the input stream", e); mErrorState = ERROR_FILE_IO_EXCEPTION; } } else { // we've got confirmation that the image size was sent mImageSizeSent = true; } } else { // if the CONTROL POINT characteristic was written just set the flag to true mRequestCompleted = true; } } else { loge("Characteristic write error: " + status); mErrorState = ERROR_CONNECTION_MASK | status; } // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } }; @Override public void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { final int responseType = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); switch (responseType) { case OP_CODE_PACKET_RECEIPT_NOTIF_KEY: final BluetoothGattCharacteristic packetCharacteristic = gatt.getService(DFU_SERVICE_UUID) .getCharacteristic(DFU_PACKET_UUID); try { mBytesConfirmed = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 1); mPacketsSendSinceNotification = 0; waitIfPaused(); if (mAborted) break; final byte[] buffer = mBuffer; final int size = mHexInputStream.readPacket(buffer); writePacket(gatt, packetCharacteristic, buffer, size); updateProgressNotification(); return; } catch (final HexFileValidationException e) { loge("Invalid HEX file"); mErrorState = ERROR_FILE_INVALID; } catch (final IOException e) { loge("Error while reading the input stream", e); mErrorState = ERROR_FILE_IO_EXCEPTION; } break; default: mReceivedData = characteristic.getValue(); break; } // notify waiting thread synchronized (mLock) { mLock.notifyAll(); return; } }; }; public DfuService() { super(TAG); } @Override public void onCreate() { super.onCreate(); final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); manager.registerReceiver(mDfuActionReceiver, makeDfuActionIntentFilter()); } @Override public void onDestroy() { super.onDestroy(); final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); manager.unregisterReceiver(mDfuActionReceiver); } @Override protected void onHandleIntent(final Intent intent) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); // In order to let DfuActivity know whether DFU is in progress, we have to use Shared Preferences final SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean(PREFS_DFU_IN_PROGRESS, true); editor.commit(); initialize(); final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); final String deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); final String filePath = intent.getStringExtra(EXTRA_FILE_PATH); final Uri fileUri = intent.getParcelableExtra(EXTRA_FILE_URI); final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI); mLogSession = Logger.openSession(this, logUri); mDeviceAddress = deviceAddress; mDeviceName = deviceName; mConnectionState = STATE_DISCONNECTED; // read preferences final boolean packetReceiptNotificationEnabled = preferences .getBoolean(SettingsFragment.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, true); final String value = preferences.getString(SettingsFragment.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(SettingsFragment.SETTINGS_NUMBER_OF_PACKETS_DEFAULT)); int numberOfPackets = SettingsFragment.SETTINGS_NUMBER_OF_PACKETS_DEFAULT; try { numberOfPackets = Integer.parseInt(value); if (numberOfPackets < 0 || numberOfPackets > 0xFFFF) numberOfPackets = SettingsFragment.SETTINGS_NUMBER_OF_PACKETS_DEFAULT; } catch (final NumberFormatException e) { numberOfPackets = SettingsFragment.SETTINGS_NUMBER_OF_PACKETS_DEFAULT; } if (!packetReceiptNotificationEnabled) numberOfPackets = 0; mPacketsBeforeNotification = numberOfPackets; sendLogBroadcast(Level.VERBOSE, "Starting DFU service"); HexInputStream his = null; try { // Prepare data to send, calculate stream size try { sendLogBroadcast(Level.VERBOSE, "Opening file..."); if (fileUri != null) his = openInputStream(fileUri); else his = openInputStream(filePath); mImageSizeInBytes = his.sizeInBytes(); mImageSizeInPackets = his.sizeInPackets(MAX_PACKET_SIZE); mHexInputStream = his; sendLogBroadcast(Level.INFO, "Image file opened (" + mImageSizeInBytes + " bytes)"); } catch (final FileNotFoundException e) { loge("An exception occured while opening file", e); sendErrorBroadcast(ERROR_FILE_NOT_FOUND); return; } catch (final IOException e) { loge("An exception occured while calculating file size", e); sendErrorBroadcast(ERROR_FILE_CLOSED); return; } // Let's connect to the device sendLogBroadcast(Level.VERBOSE, "Connecting to DFU target..."); updateProgressNotification(PROGRESS_CONNECTING); final BluetoothGatt gatt = connect(deviceAddress); // Are we connected? if (mErrorState > 0) { // error occurred final int error = mErrorState & ~ERROR_CONNECTION_MASK; loge("An error occurred while connecting to the device:" + error); sendLogBroadcast(Level.ERROR, String.format("Connection failed (0x%02X): %s", error, GattError.parse(error))); terminateConnection(gatt, mErrorState); return; } if (mAborted) { logi("Upload aborted"); sendLogBroadcast(Level.WARNING, "Upload aborted"); terminateConnection(gatt, PROGRESS_ABORTED); return; } // We have connected to DFU device and services are discoverer final BluetoothGattService dfuService = gatt.getService(DFU_SERVICE_UUID); // there was a case when the service was null. I don't know why if (dfuService == null) { loge("DFU service does not exists on the device"); sendLogBroadcast(Level.WARNING, "Connected. DFU Service not found"); terminateConnection(gatt, ERROR_SERVICE_NOT_FOUND); return; } final BluetoothGattCharacteristic controlPointCharacteristic = dfuService .getCharacteristic(DFU_CONTROL_POINT_UUID); final BluetoothGattCharacteristic packetCharacteristic = dfuService.getCharacteristic(DFU_PACKET_UUID); if (controlPointCharacteristic == null || packetCharacteristic == null) { loge("DFU characteristics not found in the DFU service"); sendLogBroadcast(Level.WARNING, "Connected. DFU Characteristics not found"); terminateConnection(gatt, ERROR_CHARACTERISTICS_NOT_FOUND); return; } sendLogBroadcast(Level.INFO, "Connected. Services discovered"); try { // enable notifications updateProgressNotification(PROGRESS_STARTING); setCharacteristicNotification(gatt, controlPointCharacteristic, true); sendLogBroadcast(Level.INFO, "Notifications enabled"); try { // set up the temporary variable that will hold the responses byte[] response = null; // send Start DFU command to Control Point logi("Sending Start DFU command (Op Code = 1)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_START_DFU); sendLogBroadcast(Level.INFO, "DFU Start sent (Op Code 1) "); // send image size in bytes to DFU Packet logi("Sending image size in bytes to DFU Packet"); writeImageSize(gatt, packetCharacteristic, mImageSizeInBytes); sendLogBroadcast(Level.INFO, "Firmware image size sent"); // a notification will come with confirmation. Let's wait for it a bit response = readNotificationResponse(); /* * The response received from the DFU device contains: * +---------+--------+----------------------------------------------------+ * | byte no | value | description | * +---------+--------+----------------------------------------------------+ * | 0 | 16 | Response code | * | 1 | 1 | The Op Code of a request that this response is for | * | 2 | STATUS | See DFU_STATUS_* for status codes | * +---------+--------+----------------------------------------------------+ */ int status = getStatusCode(response, OP_CODE_RECEIVE_START_DFU_KEY); sendLogBroadcast(Level.INFO, "Responce received (Op Code: " + response[1] + " Status: " + status + ")"); if (status != DFU_STATUS_SUCCESS) throw new RemoteDfuException("Starting DFU failed", status); // Send the number of packets of firmware before receiving a receipt notification final int numberOfPacketsBeforeNotification = mPacketsBeforeNotification; if (numberOfPacketsBeforeNotification > 0) { logi("Sending the number of packets before notifications (Op Code = 8)"); setNumberOfPackets(OP_CODE_PACKET_RECEIPT_NOTIF_REQ, numberOfPacketsBeforeNotification); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_PACKET_RECEIPT_NOTIF_REQ); sendLogBroadcast(Level.INFO, "Packet Receipt Notif Req (Op Code 8) sent (value: " + mPacketsBeforeNotification + ")"); } // Initialize firmware upload logi("Sending Receive Firmware Image request (Op Code = 3)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_RECEIVE_FIRMWARE_IMAGE); sendLogBroadcast(Level.INFO, "Receive Firmware Image request sent"); // This allow us to calculate upload time final long startTime = System.currentTimeMillis(); updateProgressNotification(); try { sendLogBroadcast(Level.INFO, "Starting upload..."); response = uploadFirmwareImage(gatt, packetCharacteristic, his); } catch (final DeviceDisconnectedException e) { loge("Disconnected while sending data"); throw e; // TODO reconnect? } final long endTime = System.currentTimeMillis(); logi("Transfer of " + mBytesSent + " bytes has taken " + (endTime - startTime) + " ms"); sendLogBroadcast(Level.INFO, "Upload completed in " + (endTime - startTime) + " ms"); // Check the result of the operation status = getStatusCode(response, OP_CODE_RECEIVE_FIRMWARE_IMAGE_KEY); sendLogBroadcast(Level.INFO, "Responce received (Op Code: " + response[1] + " Status: " + status + ")"); logi("Response received. Op Code: " + response[0] + " Req Op Code: " + response[1] + " status: " + response[2]); if (status != DFU_STATUS_SUCCESS) throw new RemoteDfuException("Device returned error after sending file", status); // Send Validate request logi("Sending Validate request (Op Code = 4)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_VALIDATE); sendLogBroadcast(Level.INFO, "Validate request sent"); // A notification will come with status code. Let's wait for it a bit. response = readNotificationResponse(); status = getStatusCode(response, OP_CODE_RECEIVE_VALIDATE_KEY); sendLogBroadcast(Level.INFO, "Responce received (Op Code: " + response[1] + " Status: " + status + ")"); if (status != DFU_STATUS_SUCCESS) throw new RemoteDfuException("Device returned validation error", status); // Disable notifications locally (we don't need to disable them on the device, it will reset) updateProgressNotification(PROGRESS_DISCONNECTING); gatt.setCharacteristicNotification(controlPointCharacteristic, false); sendLogBroadcast(Level.INFO, "Notifications disabled"); // Send Activate and Reset signal. logi("Sending Activate and Reset request (Op Code = 5)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_ACTIVATE_AND_RESET); sendLogBroadcast(Level.INFO, "Activate and Reset request sent"); // The device will reset so we don't have to send Disconnect signal. waitUntilDisconnected(); sendLogBroadcast(Level.INFO, "Disconnected by remote device"); // Close the device refreshDeviceCache(gatt); close(gatt); updateProgressNotification(PROGRESS_COMPLETED); } catch (final UnknownResponseException e) { final int error = ERROR_INVALID_RESPONSE; loge(e.getMessage()); sendLogBroadcast(Level.ERROR, e.getMessage()); // This causes GATT_ERROR 0x85 on Nexus 4 (4.4.2) // logi("Sending Reset command (Op Code = 6)"); // writeOpCode(gatt, controlPointCharacteristic, OP_CODE_RESET); // sendLogBroadcast(Level.INFO, "Reset request sent"); terminateConnection(gatt, error); } catch (final RemoteDfuException e) { final int error = ERROR_REMOTE_MASK | e.getErrorNumber(); loge(e.getMessage()); sendLogBroadcast(Level.ERROR, String.format("Remote DFU error: %s", GattError.parse(error))); // This causes GATT_ERROR 0x85 on Nexus 4 (4.4.2) // logi("Sending Reset command (Op Code = 6)"); // writeOpCode(gatt, controlPointCharacteristic, OP_CODE_RESET); // sendLogBroadcast(Level.INFO, "Reset request sent"); terminateConnection(gatt, error); } } catch (final UploadAbortedException e) { logi("Upload aborted"); sendLogBroadcast(Level.WARNING, "Upload aborted"); if (mConnectionState == STATE_CONNECTED_AND_READY) try { mAborted = false; logi("Sending Reset command (Op Code = 6)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_RESET); sendLogBroadcast(Level.INFO, "Reset request sent"); } catch (final Exception e1) { // do nothing } terminateConnection(gatt, PROGRESS_ABORTED); } catch (final DeviceDisconnectedException e) { sendLogBroadcast(Level.ERROR, "Device has disconneted"); // TODO reconnect n times? loge(e.getMessage()); if (mNotificationsEnabled) gatt.setCharacteristicNotification(controlPointCharacteristic, false); close(gatt); updateProgressNotification(ERROR_DEVICE_DISCONNECTED); return; } catch (final DfuException e) { final int error = e.getErrorNumber() & ~ERROR_CONNECTION_MASK; sendLogBroadcast(Level.ERROR, String.format("Error (0x%02X): %s", error, GattError.parse(error))); loge(e.getMessage()); if (mConnectionState == STATE_CONNECTED_AND_READY) try { logi("Sending Reset command (Op Code = 6)"); writeOpCode(gatt, controlPointCharacteristic, OP_CODE_RESET); } catch (final Exception e1) { // do nothing } terminateConnection(gatt, e.getErrorNumber()); } } finally { try { // upload has finished (success of fail) editor.putBoolean(PREFS_DFU_IN_PROGRESS, false); editor.commit(); // ensure that input stream is always closed mHexInputStream = null; if (his != null) his.close(); his = null; } catch (IOException e) { // do nothing } } } /** * 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. */ private void setNumberOfPackets(final byte[] data, final int value) { data[1] = (byte) (value & 0xFF); data[2] = (byte) ((value >> 8) & 0xFF); } /** * Opens the binary input stream from a HEX file. A Path to the HEX file is given * * @param filePath * the path to the HEX file * @return the binary input stream with Intel HEX data * @throws FileNotFoundException */ private HexInputStream openInputStream(final String filePath) throws FileNotFoundException, IOException { final InputStream is = new FileInputStream(filePath); return new HexInputStream(is); } /** * Opens the binary input stream from a HEX file. A Uri to the stream is given * * @param stream * the Uri to the stream * @return the binary input stream with Intel HEX data * @throws FileNotFoundException */ private HexInputStream openInputStream(final Uri stream) throws FileNotFoundException, IOException { if (stream.getScheme().equals("http")) { return new HexInputStream(new HttpGet(stream.toString())); } return new HexInputStream(getContentResolver().openInputStream(stream)); } /** * Connects to the BLE device with given address. This method is SYNCHRONOUS, it wait until the connection status change from {@link #STATE_CONNECTING} to {@link #STATE_CONNECTED_AND_READY} or an * error occurs. * * @param address * the device address * @return the GATT device */ private BluetoothGatt connect(final String address) { mConnectionState = STATE_CONNECTING; logi("Connecting to the device..."); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); BluetoothGatt gatt = device.connectGatt(this, false, normalCallback); // We have to wait until the device is connected and services are discovered // Connection error may occur as well. try { synchronized (mLock) { while (((mConnectionState == STATE_CONNECTING || mConnectionState == STATE_CONNECTED) && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } mConnectionState = STATE_CONNECTING; refreshDeviceCache(gatt); gatt = device.connectGatt(this, false, mGattCallback); try { synchronized (mLock) { while (((mConnectionState == STATE_CONNECTING || mConnectionState == STATE_CONNECTED) && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } return gatt; } /** * Disconnects from the device and cleans local variables in case of error. This method is SYNCHRONOUS and wait until the disconnecting process will be completed. * * @param gatt * the GATT device to be disconnected * @param error * error number */ private void terminateConnection(final BluetoothGatt gatt, final int error) { if (mConnectionState != STATE_DISCONNECTED) { updateProgressNotification(PROGRESS_DISCONNECTING); // disable notifications try { final BluetoothGattService dfuService = gatt.getService(DFU_SERVICE_UUID); if (dfuService != null) { final BluetoothGattCharacteristic controlPointCharacteristic = dfuService .getCharacteristic(DFU_CONTROL_POINT_UUID); setCharacteristicNotification(gatt, controlPointCharacteristic, false); sendLogBroadcast(Level.INFO, "Notifications disabled"); } } catch (final DeviceDisconnectedException e) { // do nothing } catch (final DfuException e) { // do nothing } catch (final Exception e) { // do nothing } // Disconnect from the device disconnect(gatt); sendLogBroadcast(Level.INFO, "Disconnected"); } // Close the device refreshDeviceCache(gatt); close(gatt); updateProgressNotification(error); } /** * Disconnects from the device. This is SYNCHRONOUS method and waits until the callback returns new state. Terminates immediately if device is already disconnected. Do not call this method * directly, use {@link #terminateConnection(BluetoothGatt, int)} instead. * * @param gatt * the GATT device that has to be disconnected */ private void disconnect(final BluetoothGatt gatt) { if (mConnectionState == STATE_DISCONNECTED) return; mConnectionState = STATE_DISCONNECTING; logi("Disconnecting from the device..."); gatt.disconnect(); // We have to wait until device gets disconnected or an error occur waitUntilDisconnected(); } /** * Wait until the connection state will change to {@link #STATE_DISCONNECTED} or until an error occurs. */ private void waitUntilDisconnected() { try { synchronized (mLock) { while (mConnectionState != STATE_DISCONNECTED && mErrorState == 0) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } } /** * Closes the GATT device and cleans up. * * @param gatt * the GATT device to be closed */ private void close(final BluetoothGatt gatt) { logi("Cleaning up..."); gatt.close(); mConnectionState = STATE_CLOSED; } /** * Clears the device cache. After uploading new firmware the DFU target will have other services than before. * * @param gatt * the GATT device to be refreshed */ private void refreshDeviceCache(final BluetoothGatt gatt) { /* * There is a refresh() method in BluetoothGatt class but for now it's hidden. We will call it using reflections. */ try { final Method refresh = gatt.getClass().getMethod("refresh"); if (refresh != null) { final boolean success = (Boolean) refresh.invoke(gatt); logi("Refreshing result: " + success); } } catch (Exception e) { loge("An exception occured while refreshing device", e); } } /** * 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 */ 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) throw new UnknownResponseException("Invalid response received", response, request); return response[2]; } /** * Enables or disables the notifications for given characteristic. This method is SYNCHRONOUS and wait until the * {@link BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int)} will be called or the connection state will change from {@link #STATE_CONNECTED_AND_READY}. If * connection state will change, or an error will occur, an exception will be thrown. * * @param gatt * the GATT device * @param characteristic * the characteristic to enable or disable notifications for * @param enable * <code>true</code> to enable notifications, <code>false</code> to disable them * @throws DfuException * @throws UploadAbortedException */ private void setCharacteristicNotification(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final boolean enable) throws DeviceDisconnectedException, DfuException, UploadAbortedException { if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Unable to set notifications state", mConnectionState); mErrorState = 0; if (mNotificationsEnabled == enable) return; logi((enable ? "Enabling " : "Disabling") + " notifications..."); // enable notifications locally gatt.setCharacteristicNotification(characteristic, enable); // enable notifications on the device final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG); descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); // We have to wait until device gets disconnected or an error occur try { synchronized (mLock) { while ((mNotificationsEnabled != enable && mConnectionState == STATE_CONNECTED_AND_READY && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } if (mAborted) throw new UploadAbortedException(); if (mErrorState != 0) throw new DfuException("Unable to set notifications state", mErrorState); if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Unable to set notifications state", mConnectionState); } /** * Writes the operation code to the characteristic. This method is SYNCHRONOUS and wait until the * {@link BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)} will be called or the connection state will change from {@link #STATE_CONNECTED_AND_READY}. * If connection state will change, or an error will occur, an exception will be thrown. * * @param gatt * the GATT device * @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 BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final byte[] value) throws DeviceDisconnectedException, DfuException, UploadAbortedException { mReceivedData = null; mErrorState = 0; mRequestCompleted = false; characteristic.setValue(value); gatt.writeCharacteristic(characteristic); // We have to wait for confirmation try { synchronized (mLock) { while ((mRequestCompleted == false && mConnectionState == STATE_CONNECTED_AND_READY && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } if (mAborted) throw new UploadAbortedException(); if (mErrorState != 0) throw new DfuException("Unable to write Op Code " + value[0], mErrorState); if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Unable to write Op Code " + value[0], mConnectionState); } /** * Writes the image size to the characteristic. This method is SYNCHRONOUS and wait until the {@link BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)} * will be called or the connection state will change from {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown. * * @param gatt * the GATT device * @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 */ private void writeImageSize(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int imageSize) throws DeviceDisconnectedException, DfuException, UploadAbortedException { mReceivedData = null; mErrorState = 0; mImageSizeSent = false; characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); characteristic.setValue(imageSize, BluetoothGattCharacteristic.FORMAT_UINT32, 0); gatt.writeCharacteristic(characteristic); // We have to wait for confirmation try { synchronized (mLock) { while ((mImageSizeSent == false && mConnectionState == STATE_CONNECTED_AND_READY && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } if (mAborted) throw new UploadAbortedException(); if (mErrorState != 0) throw new DfuException("Unable to write Image Size", mErrorState); if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Unable to write Image Size", mConnectionState); } /** * Starts sending the data. This method is SYNCHRONOUS and terminates when the whole file will be uploaded or the connection status will change from {@link #STATE_CONNECTED_AND_READY}. If * connection state will change, or an error will occur, an exception will be thrown. * * @param gatt * the GATT device (DFU target) * @param packetCharacteristic * the characteristic to write file content to. Must be the DFU PACKET * @return The response value received from notification with Op Code = 3 when all bytes will be uploaded successfully. * @throws DeviceDisconnectedException * Thrown when the device will disconnect in the middle of the transmission. The error core will be saved in {@link #mConnectionState}. * @throws DfuException * Thrown if DFU error occur * @throws UploadAbortedException */ private byte[] uploadFirmwareImage(final BluetoothGatt gatt, final BluetoothGattCharacteristic packetCharacteristic, final HexInputStream inputStream) throws DeviceDisconnectedException, DfuException, UploadAbortedException { mReceivedData = null; mErrorState = 0; final byte[] buffer = mBuffer; try { final int size = inputStream.readPacket(buffer); writePacket(gatt, packetCharacteristic, buffer, size); } catch (final HexFileValidationException e) { throw new DfuException("HEX file not valid", ERROR_FILE_INVALID); } catch (final IOException e) { throw new DfuException("Error while reading file", ERROR_FILE_IO_EXCEPTION); } try { synchronized (mLock) { while ((mReceivedData == null && mConnectionState == STATE_CONNECTED_AND_READY && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } if (mAborted) throw new UploadAbortedException(); if (mErrorState != 0) throw new DfuException("Uploading Fimrware Image failed", mErrorState); if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Uploading Fimrware Image failed: device disconnected", mConnectionState); return mReceivedData; } /** * 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. * * @param gatt * the GATT device * @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; if (buffer.length != size) { locBuffer = new byte[size]; System.arraycopy(buffer, 0, locBuffer, 0, size); } 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 (Andorid 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. } private void waitIfPaused() { synchronized (mLock) { try { while (mPaused) mLock.wait(); } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } } } /** * Waits until the notification will arrive. Returns the data returned by the notification. This method will block the thread if response is not ready or connection state will change from * {@link #STATE_CONNECTED_AND_READY}. If connection state will change, or an error will occur, an exception will be thrown. * * @return the value returned by the Control Point notification * @throws DeviceDisconnectedException * @throws DfuException * @throws UploadAbortedException */ private byte[] readNotificationResponse() throws DeviceDisconnectedException, DfuException, UploadAbortedException { mErrorState = 0; try { synchronized (mLock) { while ((mReceivedData == null && mConnectionState == STATE_CONNECTED_AND_READY && mErrorState == 0 && !mAborted) || mPaused) mLock.wait(); } } catch (final InterruptedException e) { loge("Sleeping interrupted", e); } if (mAborted) throw new UploadAbortedException(); if (mErrorState != 0) throw new DfuException("Unable to write Op Code", mErrorState); if (mConnectionState != STATE_CONNECTED_AND_READY) throw new DeviceDisconnectedException("Unable to write Op Code", mConnectionState); return mReceivedData; } /** Stores the last progress percent. Used to lower number of calls of {@link #updateProgressNotification(int)}. */ private int mLastProgress = -1; /** * Creates or updates the notification in the Notification Manager. Sends broadcast with current progress to the activity. */ private void updateProgressNotification() { final int progress = (int) (100.0f * mBytesSent / mImageSizeInBytes); if (mLastProgress == progress) return; mLastProgress = progress; updateProgressNotification(progress); } /** * Creates or updates the notification in the Notification Manager. Sends broadcast with given progress or error state to the activity. * * @param progress * the current progress state or an error number, can be one of {@link #PROGRESS_CONNECTING}, {@link #PROGRESS_STARTING}, {@link #PROGRESS_VALIDATING}, {@link #PROGRESS_DISCONNECTING}, * {@link #PROGRESS_COMPLETED} or {@link #ERROR_FILE_CLOSED}, {@link #ERROR_FILE_INVALID} , etc */ private void updateProgressNotification(final int progress) { final String deviceAddress = mDeviceAddress; final String deviceName = mDeviceName != null ? mDeviceName : getString(R.string.dfu_unknown_name); final Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.stat_dfu); final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(android.R.drawable.stat_sys_upload).setOnlyAlertOnce(true).setLargeIcon(largeIcon); switch (progress) { case PROGRESS_CONNECTING: builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_connecting)) .setContentText(getString(R.string.dfu_status_connecting_msg, deviceName)) .setProgress(100, 0, true); break; case PROGRESS_STARTING: builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_starting)) .setContentText(getString(R.string.dfu_status_starting_msg, deviceName)) .setProgress(100, 0, true); break; case PROGRESS_VALIDATING: builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_validating)) .setContentText(getString(R.string.dfu_status_validating_msg, deviceName)) .setProgress(100, 0, true); break; case PROGRESS_DISCONNECTING: builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_disconnecting)) .setContentText(getString(R.string.dfu_status_disconnecting_msg, deviceName)) .setProgress(100, 0, true); break; case PROGRESS_COMPLETED: builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_completed)) .setContentText(getString(R.string.dfu_status_completed_msg)).setAutoCancel(true); break; case PROGRESS_ABORTED: builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_abored)) .setContentText(getString(R.string.dfu_status_aborted_msg)).setAutoCancel(true); break; default: if (progress >= ERROR_MASK) { // progress is an error number builder.setOngoing(false).setContentTitle(getString(R.string.dfu_status_error)) .setContentText(getString(R.string.dfu_status_error_msg)).setAutoCancel(true); } else { // progress is in percents builder.setOngoing(true).setContentTitle(getString(R.string.dfu_status_uploading)) .setContentText(getString(R.string.dfu_status_uploading_msg, deviceName)) .setProgress(100, progress, false); } } // send progress or error broadcast if (progress < ERROR_MASK) sendProgressBroadcast(progress); else sendErrorBroadcast(progress); // We cannot set two activities at once (using PendingIntent.getActivities(...)) because we have to start the BluetoothLeService first. Service is created in DeviceListActivity. // When creating activities the parent Activity is not created, it's just inserted to the history stack. final Intent intent = new Intent(this, NotificationActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(DfuActivity.EXTRA_DEVICE_ADDRESS, deviceAddress); intent.putExtra(DfuActivity.EXTRA_DEVICE_NAME, deviceName); intent.putExtra(DfuActivity.EXTRA_PROGRESS, progress); // this may contains ERROR_CONNECTION_MASK bit! if (mLogSession != null) intent.putExtra(DfuActivity.EXTRA_LOG_URI, mLogSession.getSessionUri()); final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(NOTIFICATION_ID, builder.build()); } private void sendProgressBroadcast(final int progress) { final Intent broadcast = new Intent(BROADCAST_PROGRESS); broadcast.putExtra(EXTRA_DATA, progress); broadcast.putExtra(EXTRA_DEVICE_ADDRESS, mDeviceAddress); if (mLogSession != null) broadcast.putExtra(EXTRA_LOG_URI, mLogSession.getSessionUri()); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } private void sendErrorBroadcast(final int error) { final Intent broadcast = new Intent(BROADCAST_ERROR); broadcast.putExtra(EXTRA_DATA, error & ~ERROR_CONNECTION_MASK); broadcast.putExtra(EXTRA_DEVICE_ADDRESS, mDeviceAddress); if (mLogSession != null) broadcast.putExtra(EXTRA_LOG_URI, mLogSession.getSessionUri()); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } private void sendLogBroadcast(final int level, final String message) { final LogSession session = mLogSession; final String fullMessage = "[DFU] " + message; if (session == null) { // the log provider is not installed, use broadcast action final Intent broadcast = new Intent(BROADCAST_LOG); broadcast.putExtra(EXTRA_LOG_MESSAGE, fullMessage); broadcast.putExtra(EXTRA_LOG_LEVEL, level); broadcast.putExtra(EXTRA_DEVICE_ADDRESS, mDeviceAddress); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } else { // the log provider is installed, we can use logger Logger.log(session, level, fullMessage); } } /** * Initializes bluetooth adapter * * @return <code>true</code> if initialization was successful */ private boolean initialize() { // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothManager. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { loge("Unable to initialize BluetoothManager."); return false; } mBluetoothAdapter = bluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { loge("Unable to obtain a BluetoothAdapter."); return false; } return true; } private static IntentFilter makeDfuActionIntentFilter() { final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DfuService.BROADCAST_ACTION); return intentFilter; } private void loge(final String message) { if (BuildConfig.DEBUG) Log.e(TAG, message); } private void loge(final String message, final Throwable e) { if (BuildConfig.DEBUG) Log.e(TAG, message, e); } private void logi(final String message) { if (BuildConfig.DEBUG) Log.i(TAG, message); } }