diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java index 339bed8..af46b1a 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/DfuBaseService.java @@ -622,6 +622,7 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres private boolean mAborted; private DfuCallback mDfuServiceImpl; + private InputStream mFirmwareInputStream, mInitFileInputStream; private final BroadcastReceiver mDfuActionReceiver = new BroadcastReceiver() { @Override @@ -879,6 +880,20 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres unregisterReceiver(mDfuActionReceiver); unregisterReceiver(mConnectionStateBroadcastReceiver); unregisterReceiver(mBondStateBroadcastReceiver); + + try { + // Ensure that input stream is always closed + if (mFirmwareInputStream != null) + mFirmwareInputStream.close(); + if (mInitFileInputStream != null) + mInitFileInputStream.close(); + } catch (final IOException e) { + // do nothing + } finally { + mFirmwareInputStream = null; + mInitFileInputStream = null; + } + logi("DFU service destroyed"); } @Override @@ -946,53 +961,65 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres /* * First the service is trying to read the firmware and init packet files. */ - InputStream is = null; - InputStream initIs = null; - int imageSizeInBytes; + InputStream is = mFirmwareInputStream; + InputStream initIs = mInitFileInputStream; try { + final boolean firstRun = mFirmwareInputStream == null; + // Prepare data to send, calculate stream size try { - sendLogBroadcast(LOG_LEVEL_VERBOSE, "Opening file..."); - if (fileUri != null) { - is = openInputStream(fileUri, mimeType, mbrSize, fileType); - } else if (filePath != null) { - is = openInputStream(filePath, mimeType, mbrSize, fileType); - } else if (fileResId > 0) { - is = openInputStream(fileResId, mimeType, mbrSize, fileType); + if (firstRun) { + // The files are opened only once, when DFU service is first started. + // In case the service needs to be restarted (for example a buttonless service + // was found or to send Application in the second connection) the input stream + // is kept as a global service field. This is to avoid SecurityException + // when the URI was granted with one-time read permission. + // See: Intent#FLAG_GRANT_READ_URI_PERMISSION (https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION). + sendLogBroadcast(LOG_LEVEL_VERBOSE, "Opening file..."); + if (fileUri != null) { + is = openInputStream(fileUri, mimeType, mbrSize, fileType); + } else if (filePath != null) { + is = openInputStream(filePath, mimeType, mbrSize, fileType); + } else if (fileResId > 0) { + is = openInputStream(fileResId, mimeType, mbrSize, fileType); + } + + // The Init file Input Stream is kept global only in case it was provided + // as an argument (separate file for HEX/BIN and DAT files). + // If a ZIP file was given with DAT file(s) inside it will be taken from the ZIP + // ~20 lines below. + if (initFileUri != null) { + // Try to read the Init Packet file from URI + initIs = getContentResolver().openInputStream(initFileUri); + } else if (initFilePath != null) { + // Try to read the Init Packet file from path + initIs = new FileInputStream(initFilePath); + } else if (initFileResId > 0) { + // Try to read the Init Packet file from given resource + initIs = getResources().openRawResource(initFileResId); + } + + final int imageSizeInBytes = is.available(); + if ((imageSizeInBytes % 4) != 0) + throw new SizeValidationException("The new firmware is not word-aligned."); } - if (initFileUri != null) { - // Try to read the Init Packet file from URI - initIs = getContentResolver().openInputStream(initFileUri); - } else if (initFilePath != null) { - // Try to read the Init Packet file from path - initIs = new FileInputStream(initFilePath); - } else if (initFileResId > 0) { - // Try to read the Init Packet file from given resource - initIs = getResources().openRawResource(initFileResId); - } - - imageSizeInBytes = is.available(); - - if ((imageSizeInBytes % 4) != 0) - throw new SizeValidationException("The new firmware is not word-aligned."); - // Update the file type bit field basing on the ZIP content - if (fileType == TYPE_AUTO && MIME_TYPE_ZIP.equals(mimeType)) { - final ArchiveInputStream zhis = (ArchiveInputStream) is; - fileType = zhis.getContentType(); - } - // Set the Init packet stream in case of a ZIP file if (MIME_TYPE_ZIP.equals(mimeType)) { final ArchiveInputStream zhis = (ArchiveInputStream) is; + if (fileType == TYPE_AUTO) { + fileType = zhis.getContentType(); + } else { + fileType = zhis.setContentType(fileType); + } - // Validate sizes - if ((fileType & TYPE_APPLICATION) > 0 && (zhis.applicationImageSize() % 4) != 0) - throw new SizeValidationException("Application firmware is not word-aligned."); - if ((fileType & TYPE_BOOTLOADER) > 0 && (zhis.bootloaderImageSize() % 4) != 0) - throw new SizeValidationException("Bootloader firmware is not word-aligned."); - if ((fileType & TYPE_SOFT_DEVICE) > 0 && (zhis.softDeviceImageSize() % 4) != 0) - throw new SizeValidationException("Soft Device firmware is not word-aligned."); + // Validate sizes + if ((fileType & TYPE_APPLICATION) > 0 && (zhis.applicationImageSize() % 4) != 0) + throw new SizeValidationException("Application firmware is not word-aligned."); + if ((fileType & TYPE_BOOTLOADER) > 0 && (zhis.bootloaderImageSize() % 4) != 0) + throw new SizeValidationException("Bootloader firmware is not word-aligned."); + if ((fileType & TYPE_SOFT_DEVICE) > 0 && (zhis.softDeviceImageSize() % 4) != 0) + throw new SizeValidationException("Soft Device firmware is not word-aligned."); if (fileType == TYPE_APPLICATION) { if (zhis.getApplicationInit() != null) @@ -1002,7 +1029,9 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres initIs = new ByteArrayInputStream(zhis.getSystemInit()); } } - sendLogBroadcast(LOG_LEVEL_INFO, "Firmware file opened (" + imageSizeInBytes + " bytes in total)"); + mFirmwareInputStream = is; + mInitFileInputStream = initIs; + sendLogBroadcast(LOG_LEVEL_INFO, "Firmware file opened successfully"); } catch (final SecurityException e) { loge("A security exception occurred while opening file", e); sendLogBroadcast(LOG_LEVEL_ERROR, "Opening file failed: Permission required"); @@ -1013,11 +1042,11 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres sendLogBroadcast(LOG_LEVEL_ERROR, "Opening file failed: File not found"); report(ERROR_FILE_NOT_FOUND); return; - } catch (final SizeValidationException e) { - loge("Firmware not word-aligned", e); + } catch (final SizeValidationException e) { + loge("Firmware not word-aligned", e); sendLogBroadcast(LOG_LEVEL_ERROR, "Opening file failed: Firmware size must be word-aligned"); - report(ERROR_FILE_SIZE_INVALID); - return; + report(ERROR_FILE_SIZE_INVALID); + return; } catch (final IOException e) { loge("An exception occurred while calculating file size", e); sendLogBroadcast(LOG_LEVEL_ERROR, "Opening file failed: " + e.getLocalizedMessage()); @@ -1030,10 +1059,12 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres return; } - // Wait a second... If we were connected before it's good to give some time before we start reconnecting. - waitFor(1000); - // Looks like a second is not enough. The ACL_DISCONNECTED broadcast sometimes comes later (on Android 7.0) - waitFor(1000); + if (!firstRun) { + // Wait a second... If we were connected before it's good to give some time before we start reconnecting. + waitFor(1000); + // Looks like a second is not enough. The ACL_DISCONNECTED broadcast sometimes comes later (on Android 7.0) + waitFor(1000); + } mProgressInfo = new DfuProgressInfo(this); @@ -1163,13 +1194,6 @@ public abstract class DfuBaseService extends IntentService implements DfuProgres } } } finally { - try { - // Ensure that input stream is always closed - if (is != null) - is.close(); - } catch (final IOException e) { - // do nothing - } if (foregroundService) { // This will stop foreground state and, if the progress notifications were disabled // it will also remove the notification indicating foreground service. diff --git a/dfu/src/main/java/no/nordicsemi/android/dfu/internal/ArchiveInputStream.java b/dfu/src/main/java/no/nordicsemi/android/dfu/internal/ArchiveInputStream.java index e033011..0b08802 100644 --- a/dfu/src/main/java/no/nordicsemi/android/dfu/internal/ArchiveInputStream.java +++ b/dfu/src/main/java/no/nordicsemi/android/dfu/internal/ArchiveInputStream.java @@ -77,6 +77,7 @@ public class ArchiveInputStream extends ZipInputStream { private byte[] systemInitBytes; private byte[] applicationInitBytes; private byte[] currentSource; + private int type; private int bytesReadFromCurrentSource; private int softDeviceSize; private int bootloaderSize; @@ -251,6 +252,7 @@ public class ArchiveInputStream extends ZipInputStream { } mark(0); } finally { + type = getContentType(); super.close(); } } @@ -372,9 +374,6 @@ public class ArchiveInputStream extends ZipInputStream { @Override public void reset() throws IOException { - if (applicationBytes != null && (softDeviceBytes != null || bootloaderBytes != null || softDeviceAndBootloaderBytes != null)) - throw new UnsupportedOperationException("Application must be sent in a separate connection."); - currentSource = markedSource; bytesRead = bytesReadFromCurrentSource = bytesReadFromMarkedSource; @@ -409,7 +408,7 @@ public class ArchiveInputStream extends ZipInputStream { * TYPE_APPLICATION} */ public int getContentType() { - byte type = 0; + 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. if (softDeviceAndBootloaderBytes != null) type |= DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER; @@ -431,33 +430,40 @@ public class ArchiveInputStream extends ZipInputStream { * @return the final type after truncating */ public int setContentType(final int type) { - if (bytesRead > 0) - throw new UnsupportedOperationException("Content type must not be change after reading content"); + this.type = type; + // If the new type has Application, but there is no application fw, remove this type bit + if ((type & DfuBaseService.TYPE_APPLICATION) > 0 && applicationBytes == null) + this.type &= ~DfuBaseService.TYPE_APPLICATION; + // If the new type has SD+BL + if ((type & (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) == (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) { + // but there is no SD, remove the softdevice type bit + if (softDeviceBytes == null && softDeviceAndBootloaderBytes == null) + this.type &= ~DfuBaseService.TYPE_SOFT_DEVICE; + // or there is no BL, remove the bootloader type bit + if (bootloaderBytes == null && softDeviceAndBootloaderBytes == null) + this.type &= ~DfuBaseService.TYPE_SOFT_DEVICE; + } else { + // If at least one of SD or B: bit is cleared, but the SD+BL file is set, remove both bits. + if (softDeviceAndBootloaderBytes != null) + this.type &= ~(DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER); + } - final int t = getContentType() & type; - - if ((t & DfuBaseService.TYPE_SOFT_DEVICE) == 0) { - softDeviceBytes = null; - if (softDeviceAndBootloaderBytes != null) { - softDeviceAndBootloaderBytes = null; - bootloaderSize = 0; - } - softDeviceSize = 0; + if ((type & (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) > 0 && softDeviceAndBootloaderBytes != null) + currentSource = softDeviceAndBootloaderBytes; + else if ((type & DfuBaseService.TYPE_SOFT_DEVICE) > 0) + currentSource = softDeviceBytes; + else if ((type & DfuBaseService.TYPE_BOOTLOADER) > 0) + currentSource = bootloaderBytes; + else if ((type & DfuBaseService.TYPE_APPLICATION) > 0) + currentSource = applicationBytes; + bytesReadFromCurrentSource = 0; + try { + mark(0); + reset(); + } catch (final IOException e) { + // not available } - if ((t & DfuBaseService.TYPE_BOOTLOADER) == 0) { - bootloaderBytes = null; - if (softDeviceAndBootloaderBytes != null) { - softDeviceAndBootloaderBytes = null; - softDeviceSize = 0; - } - bootloaderSize = 0; - } - if ((t & DfuBaseService.TYPE_APPLICATION) == 0) { - applicationBytes = null; - applicationSize = 0; - } - mark(0); - return t; + return this.type; } /** @@ -467,9 +473,9 @@ public class ArchiveInputStream extends ZipInputStream { */ private byte[] startNextFile() { byte[] ret; - if (currentSource == softDeviceBytes && bootloaderBytes != null) { + if (currentSource == softDeviceBytes && bootloaderBytes != null && (type & DfuBaseService.TYPE_BOOTLOADER) > 0) { ret = currentSource = bootloaderBytes; - } else if (currentSource != applicationBytes && applicationBytes != null) { + } else if (currentSource != applicationBytes && applicationBytes != null && (type & DfuBaseService.TYPE_APPLICATION) > 0) { ret = currentSource = applicationBytes; } else { ret = currentSource = null; @@ -488,11 +494,12 @@ public class ArchiveInputStream extends ZipInputStream { // This method then is just used to log file size. // In case of SD+BL in Secure DFU: - if (softDeviceAndBootloaderBytes != null && softDeviceSize == 0 && bootloaderSize == 0) - return softDeviceAndBootloaderBytes.length + applicationSize - bytesRead; + if (softDeviceAndBootloaderBytes != null && softDeviceSize == 0 && bootloaderSize == 0 + && (type & (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) > 0) + return softDeviceAndBootloaderBytes.length + applicationImageSize() - bytesRead; // Otherwise: - return softDeviceSize + bootloaderSize + applicationSize - bytesRead; + return softDeviceImageSize() + bootloaderImageSize() + applicationImageSize() - bytesRead; } /** @@ -500,7 +507,7 @@ public class ArchiveInputStream extends ZipInputStream { * @return the size of the SoftDevice firmware (BIN part) */ public int softDeviceImageSize() { - return softDeviceSize; + return (type & DfuBaseService.TYPE_SOFT_DEVICE) > 0 ? softDeviceSize : 0; } /** @@ -508,7 +515,7 @@ public class ArchiveInputStream extends ZipInputStream { * @return the size of the Bootloader firmware (BIN part) */ public int bootloaderImageSize() { - return bootloaderSize; + return (type & DfuBaseService.TYPE_BOOTLOADER) > 0 ? bootloaderSize : 0; } /** @@ -516,7 +523,7 @@ public class ArchiveInputStream extends ZipInputStream { * @return the size of the Application firmware (BIN part) */ public int applicationImageSize() { - return applicationSize; + return (type & DfuBaseService.TYPE_APPLICATION) > 0 ? applicationSize : 0; } /**