diff --git a/build.gradle b/build.gradle index 816b54c2..51577252 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' } } diff --git a/mcumgr-android-lib/build.gradle b/mcumgr-android-lib/build.gradle index 9b31b5c8..ec6993ec 100644 --- a/mcumgr-android-lib/build.gradle +++ b/mcumgr-android-lib/build.gradle @@ -10,11 +10,11 @@ apply plugin: 'com.github.dcendents.android-maven' group='com.github.runtimeco' android { - compileSdkVersion 27 + compileSdkVersion 28 buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 21 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName mcuMgrVersion @@ -35,10 +35,11 @@ dependencies { androidTestImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'junit:junit:4.12' - implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:appcompat-v7:28.0.0-rc01' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.9.6' implementation 'com.fasterxml.jackson.core:jackson-core:2.9.6' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + implementation 'com.jakewharton.timber:timber:4.7.1' } // build a jar with source files diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/McuManager.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/McuManager.java index 778248c7..9f95982e 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/McuManager.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/McuManager.java @@ -9,7 +9,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import java.io.IOException; import java.text.ParseException; @@ -23,13 +22,13 @@ import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.response.McuMgrResponse; import io.runtime.mcumgr.util.CBOR; +import timber.log.Timber; /** * TODO */ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class McuManager { - private static final String TAG = McuManager.class.getSimpleName(); // Transport constants private final static int DEFAULT_MTU = 515; @@ -123,10 +122,10 @@ public McuMgrTransport getTransporter() { */ public synchronized boolean setUploadMtu(int mtu) { if (mtu < 23) { - Log.e(TAG, "MTU is too small!"); + Timber.e("MTU is too small! Must be greater than 23."); return false; } else if (mtu > 1024) { - Log.e(TAG, "MTU is too large!"); + Timber.e("MTU is too large! Must be less than 1024."); return false; } else { mMtu = mtu; @@ -365,7 +364,7 @@ public static Date stringToDate(@Nullable String dateString) { try { return mcumgrFormat.parse(dateString); } catch (ParseException e) { - Log.e(TAG, "Converting string to Date failed", e); + Timber.e(e, "Converting string to Date failed"); return null; } } diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeController.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeController.java index 89094fed..3cb18d57 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeController.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeController.java @@ -21,6 +21,12 @@ public interface FirmwareUpgradeController { /** * Cancel the firmware upgrade. + * The firmware may be cancelled in + * {@link FirmwareUpgradeManager.State#VALIDATE} or + * {@link FirmwareUpgradeManager.State#UPLOAD} state. + * The manager does not try to recover the original firmware after the test or confirm commands + * have been sent. To undo the upload, confirm the image that have been moved to slot 1 during + * swap. */ void cancel(); diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeManager.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeManager.java index ae8ee701..eeef11d0 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeManager.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/dfu/FirmwareUpgradeManager.java @@ -11,7 +11,6 @@ import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import java.util.Arrays; import java.util.concurrent.Executor; @@ -25,6 +24,7 @@ import io.runtime.mcumgr.managers.ImageManager; import io.runtime.mcumgr.response.McuMgrResponse; import io.runtime.mcumgr.response.img.McuMgrImageStateResponse; +import timber.log.Timber; // TODO Add retries for each step @@ -46,7 +46,6 @@ */ @SuppressWarnings({"WeakerAccess", "unused"}) public class FirmwareUpgradeManager implements FirmwareUpgradeController { - private final static String TAG = "FirmwareUpgradeManager"; public enum Mode { /** @@ -176,7 +175,7 @@ public void setFirmwareUpgradeCallback(@Nullable FirmwareUpgradeCallback callbac */ public void setMode(@NonNull Mode mode) { if (mState != State.NONE) { - Log.i(TAG, "Firmware upgrade is already in progress"); + Timber.i("Firmware upgrade is already in progress"); return; } mMode = mode; @@ -202,7 +201,7 @@ public void setUploadMtu(int mtu) { */ public synchronized void start(@NonNull byte[] imageData) throws McuMgrException { if (mState != State.NONE) { - Log.i(TAG, "Firmware upgrade is already in progress"); + Timber.i("Firmware upgrade is already in progress"); return; } // Set image and validate @@ -220,7 +219,10 @@ public synchronized void start(@NonNull byte[] imageData) throws McuMgrException @Override public synchronized void cancel() { - if (mState == State.UPLOAD) { + if (mState == State.VALIDATE) { + mState = State.NONE; + mPaused = false; + } else if (mState == State.UPLOAD) { mImageManager.cancelUpload(); mPaused = false; } @@ -229,7 +231,7 @@ public synchronized void cancel() { @Override public synchronized void pause() { if (mState.isInProgress()) { - Log.i(TAG, "Pausing upgrade."); + Timber.i("Pausing upgrade."); mPaused = true; if (mState == State.UPLOAD) { mImageManager.pauseUpload(); @@ -263,7 +265,7 @@ private synchronized void setState(State newState) { State prevState = mState; mState = newState; if (newState != prevState) { - Log.v(TAG, "Moving from state " + prevState.name() + " to state " + newState.name()); + Timber.v("Moving from state %s to state %s", prevState.name(), newState.name()); mInternalCallback.onStateChanged(prevState, newState); } } @@ -324,6 +326,13 @@ private synchronized void fail(McuMgrException error) { mInternalCallback.onUpgradeFailed(failedState, error); } + private synchronized void cancelled(State state) { + Timber.v("Upgrade cancelled!"); + mState = State.NONE; + mPaused = false; + mInternalCallback.onUpgradeCanceled(state); + } + //****************************************************************** // McuManagerCallbacks //****************************************************************** @@ -336,7 +345,7 @@ private synchronized void fail(McuMgrException error) { new McuMgrCallback<McuMgrImageStateResponse>() { @Override public void onResponse(@NonNull final McuMgrImageStateResponse response) { - Log.v(TAG, "Validation response: " + response.toString()); + Timber.v("Validation response: %s", response.toString()); // Check for an error return code if (!response.isSuccess()) { @@ -344,6 +353,11 @@ public void onResponse(@NonNull final McuMgrImageStateResponse response) { return; } + if (mState == State.NONE) { + cancelled(State.VALIDATE); + return; + } + McuMgrImageStateResponse.ImageSlot[] images = response.images; // Check if the new firmware is different than the active one. @@ -443,7 +457,7 @@ public void onError(@NonNull McuMgrException e) { private McuMgrCallback<McuMgrImageStateResponse> mTestCallback = new McuMgrCallback<McuMgrImageStateResponse>() { @Override public void onResponse(@NonNull McuMgrImageStateResponse response) { - Log.v(TAG, "Test response: " + response.toString()); + Timber.v("Test response: %s", response.toString()); // Check for an error return code if (!response.isSuccess()) { fail(new McuMgrErrorException(response.getReturnCode())); @@ -482,8 +496,12 @@ public void onConnected() { public void onDisconnected() { // Device has reset. mDefaultManager.getTransporter().removeObserver(this); - Log.v(TAG, "Reset successful"); + Timber.v("Reset successful"); switch (mState) { + case NONE: + // Upload was cancelled in VALIDATE state + cancelled(State.VALIDATE); + break; case VALIDATE: // The device has exited test mode. Slot 1 can now be erased. validate(); @@ -513,7 +531,7 @@ public void onDisconnected() { @Override public void onResponse(@NonNull McuMgrResponse response) { // Reset command has been sent. - Log.v(TAG, "Reset request sent. Waiting for reset"); + Timber.v("Reset request sent. Waiting for reset..."); // Check for an error return code if (!response.isSuccess()) { fail(new McuMgrErrorException(response.getReturnCode())); @@ -534,7 +552,7 @@ public void onError(@NonNull McuMgrException e) { new McuMgrCallback<McuMgrImageStateResponse>() { @Override public void onResponse(@NonNull McuMgrImageStateResponse response) { - Log.v(TAG, "Confirm response: " + response.toString()); + Timber.v("Confirm response: %s", response.toString()); // Check for an error return code if (!response.isSuccess()) { fail(new McuMgrErrorException(response.getReturnCode())); @@ -655,7 +673,7 @@ public void onUploadFailed(@NonNull McuMgrException error) { @Override public void onUploadCanceled() { - mInternalCallback.onUpgradeCanceled(mState); + cancelled(State.UPLOAD); } @Override diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/image/McuMgrImageHeader.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/image/McuMgrImageHeader.java index fa033091..d359e211 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/image/McuMgrImageHeader.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/image/McuMgrImageHeader.java @@ -21,7 +21,6 @@ */ @SuppressWarnings({"unused", "WeakerAccess"}) public class McuMgrImageHeader { - private static final String TAG = McuMgrImageHeader.class.getSimpleName(); private static final int IMG_HEADER_MAGIC = 0x96f3b83d; private static final int IMG_HEADER_MAGIC_V1 = 0x96f3b83c; diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/FsManager.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/FsManager.java index 8cf8c92e..41fb0ab8 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/FsManager.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/FsManager.java @@ -8,7 +8,6 @@ package io.runtime.mcumgr.managers; import android.support.annotation.NonNull; -import android.util.Log; import java.io.IOException; import java.util.HashMap; @@ -23,10 +22,10 @@ import io.runtime.mcumgr.response.fs.McuMgrFsDownloadResponse; import io.runtime.mcumgr.response.fs.McuMgrFsUploadResponse; import io.runtime.mcumgr.util.CBOR; +import timber.log.Timber; @SuppressWarnings({"WeakerAccess", "unused"}) public class FsManager extends McuManager { - private final static String TAG = "FsManager"; private final static int ID_FILE = 0; @@ -179,7 +178,7 @@ public synchronized void download(@NonNull String name, @NonNull FileDownloadCal if (mTransferState == STATE_NONE) { mTransferState = STATE_DOWNLOADING; } else { - Log.d(TAG, "FsManager is not ready"); + Timber.d("FsManager is not ready"); return; } @@ -202,7 +201,7 @@ public synchronized void upload(@NonNull String name, @NonNull byte[] data, if (mTransferState == STATE_NONE) { mTransferState = STATE_UPLOADING; } else { - Log.d(TAG, "FsManager is not ready"); + Timber.d("FsManager is not ready"); return; } @@ -245,8 +244,20 @@ public synchronized int getState() { */ public synchronized void cancelTransfer() { if (mTransferState == STATE_NONE) { - Log.d(TAG, "File transfer is not in progress"); + Timber.d("File transfer is not in progress"); + } else if (mTransferState == STATE_PAUSED) { + Timber.d("Upload canceled!"); + resetTransfer(); + if (mUploadCallback != null) { + mUploadCallback.onUploadCanceled(); + mUploadCallback = null; + } + if (mDownloadCallback != null) { + mDownloadCallback.onDownloadCanceled(); + mDownloadCallback = null; + } } else { + // Transfer will be cancelled resetTransfer(); } } @@ -256,9 +267,9 @@ public synchronized void cancelTransfer() { */ public synchronized void pauseTransfer() { if (mTransferState == STATE_NONE) { - Log.d(TAG, "File transfer is not in progress."); + Timber.d("File transfer is not in progress."); } else { - Log.d(TAG, "Upload paused."); + Timber.d("Upload paused."); mTransferState = STATE_PAUSED; } } @@ -268,7 +279,7 @@ public synchronized void pauseTransfer() { */ public synchronized void continueTransfer() { if (mTransferState == STATE_PAUSED) { - Log.d(TAG, "Continuing transfer."); + Timber.d("Continuing transfer."); if (mDownloadCallback != null) { mTransferState = STATE_DOWNLOADING; requestNext(mOffset); @@ -277,7 +288,7 @@ public synchronized void continueTransfer() { sendNext(mOffset); } } else { - Log.d(TAG, "Transfer is not paused."); + Timber.d("Transfer is not paused."); } } @@ -285,25 +296,24 @@ public synchronized void continueTransfer() { // Implementation //****************************************************************** - private synchronized void failUpload(McuMgrException error) { + private synchronized void fail(McuMgrException error) { if (mUploadCallback != null) { mUploadCallback.onUploadFailed(error); }else if (mDownloadCallback != null) { mDownloadCallback.onDownloadFailed(error); } - cancelTransfer(); + resetTransfer(); + mUploadCallback = null; + mDownloadCallback = null; } - private synchronized void restartUpload() { - if (mFileData == null || mUploadCallback == null) { - Log.e(TAG, "Could not restart upload: image data or callback is null!"); - return; + private synchronized void restartTransfer() { + mTransferState = STATE_NONE; + if (mUploadCallback != null) { + upload(mFileName, mFileData, mUploadCallback); + } else if (mDownloadCallback != null) { + download(mFileName, mDownloadCallback); } - String tempName = mFileName; - byte[] tempData = mFileData; - FileUploadCallback tempCallback = mUploadCallback; - resetTransfer(); - upload(tempName, tempData, tempCallback); } private synchronized void resetTransfer() { @@ -321,7 +331,7 @@ private synchronized void resetTransfer() { private synchronized void sendNext(int offset) { // Check that the state is STATE_UPLOADING if (mTransferState != STATE_UPLOADING) { - Log.d(TAG, "Fs Manager is not in the UPLOADING state."); + Timber.d("Fs Manager is not in the UPLOADING state."); return; } upload(mFileName, mFileData, offset, mUploadCallbackImpl); @@ -335,7 +345,7 @@ private synchronized void sendNext(int offset) { private synchronized void requestNext(int offset) { // Check that the state is STATE_UPLOADING if (mTransferState != STATE_DOWNLOADING) { - Log.d(TAG, "Fs Manager is not in the DOWNLOADING state."); + Timber.d("Fs Manager is not in the DOWNLOADING state."); return; } download(mFileName, offset, mDownloadCallbackImpl); @@ -355,14 +365,14 @@ private synchronized void requestNext(int offset) { public void onResponse(@NonNull McuMgrFsUploadResponse response) { // Check for a McuManager error if (response.rc != 0) { - Log.e(TAG, "Upload failed due to McuManager error: " + response.rc); - failUpload(new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc))); + Timber.e("Upload failed due to McuManager error: %s", response.rc); + fail(new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc))); return; } // Check if upload hasn't been cancelled. if (mTransferState == STATE_NONE) { - Log.d(TAG, "Upload canceled!"); + Timber.d("Upload canceled!"); resetTransfer(); mUploadCallback.onUploadCanceled(); mUploadCallback = null; @@ -378,7 +388,7 @@ public void onResponse(@NonNull McuMgrFsUploadResponse response) { // Check if the upload has finished. if (mOffset == mFileData.length) { - Log.d(TAG, "Upload finished!"); + Timber.d("Upload finished!"); resetTransfer(); mUploadCallback.onUploadFinished(); mUploadCallback = null; @@ -403,12 +413,12 @@ public void onError(@NonNull McuMgrException error) { if (isMtuSet) { // If the MTU has been set successfully, restart the upload. - restartUpload(); + restartTransfer(); return; } } // If the exception is not due to insufficient MTU fail the upload. - failUpload(error); + fail(error); } }; @@ -424,14 +434,14 @@ public void onError(@NonNull McuMgrException error) { public void onResponse(@NonNull McuMgrFsDownloadResponse response) { // Check for a McuManager error. if (response.rc != 0) { - Log.e(TAG, "Download failed due to McuManager error: " + response.rc); - failUpload(new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc))); + Timber.e("Download failed due to McuManager error: %s", response.rc); + fail(new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc))); return; } // Check if download hasn't been cancelled. if (mTransferState == STATE_NONE) { - Log.d(TAG, "Download canceled!"); + Timber.d("Download canceled!"); resetTransfer(); mDownloadCallback.onDownloadCanceled(); mDownloadCallback = null; @@ -456,7 +466,7 @@ public void onResponse(@NonNull McuMgrFsDownloadResponse response) { // Check if the download has finished. if (mOffset == mFileData.length) { - Log.d(TAG, "Download finished!"); + Timber.d("Download finished!"); byte[] data = mFileData; String fileName = mFileName; resetTransfer(); @@ -471,8 +481,24 @@ public void onResponse(@NonNull McuMgrFsDownloadResponse response) { @Override public void onError(@NonNull McuMgrException error) { + // Check if the exception is due to an insufficient MTU. + if (error instanceof InsufficientMtuException) { + InsufficientMtuException mtuErr = (InsufficientMtuException) error; + + // Set the MTU to the value specified in the error response. + int mtu = mtuErr.getMtu(); + if (mMtu == mtu) + mtu -= 1; + boolean isMtuSet = setUploadMtu(mtu); + + if (isMtuSet) { + // If the MTU has been set successfully, restart the upload. + restartTransfer(); + return; + } + } // If the exception is not due to insufficient MTU fail the upload. - failUpload(error); + fail(error); } }; @@ -498,7 +524,7 @@ private int calculatePacketOverhead(@NonNull String name, @NonNull byte[] data, return cborData.length + 8 + 2 + 3; } } catch (IOException e) { - Log.e(TAG, "Error while calculating packet overhead", e); + Timber.e(e, "Error while calculating packet overhead"); } return -1; } diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/ImageManager.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/ImageManager.java index 76c8cd60..7d012e9f 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/ImageManager.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/ImageManager.java @@ -9,7 +9,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import java.io.IOException; import java.security.MessageDigest; @@ -30,6 +29,7 @@ import io.runtime.mcumgr.response.img.McuMgrImageStateResponse; import io.runtime.mcumgr.response.img.McuMgrImageUploadResponse; import io.runtime.mcumgr.util.CBOR; +import timber.log.Timber; /** * Image command-group manager. This manager can read the image state of a device, test or @@ -44,7 +44,6 @@ */ @SuppressWarnings({"unused", "WeakerAccess"}) public class ImageManager extends McuManager { - private final static String TAG = "ImageManager"; private final static int IMG_HASH_LEN = 32; private final static int TRUNCATED_HASH_LEN = 3; @@ -292,7 +291,7 @@ public synchronized boolean upload(@NonNull byte[] data, @NonNull ImageUploadCal if (mUploadState == STATE_NONE) { mUploadState = STATE_UPLOADING; } else { - Log.d(TAG, "An image upload is already in progress"); + Timber.d("An image upload is already in progress"); return false; } @@ -434,9 +433,9 @@ public synchronized int getUploadState() { */ public synchronized void cancelUpload() { if (mUploadState == STATE_NONE) { - Log.d(TAG, "Image upload is not in progress"); + Timber.d("Image upload is not in progress"); } else if (mUploadState == STATE_PAUSED) { - Log.d(TAG, "Upload canceled!"); + Timber.d("Upload canceled!"); resetUpload(); mUploadCallback.onUploadCanceled(); mUploadCallback = null; @@ -449,9 +448,9 @@ public synchronized void cancelUpload() { */ public synchronized void pauseUpload() { if (mUploadState == STATE_NONE) { - Log.d(TAG, "Upload is not in progress."); + Timber.d("Upload is not in progress."); } else { - Log.d(TAG, "Upload paused."); + Timber.d("Upload paused."); mUploadState = STATE_PAUSED; } } @@ -461,11 +460,11 @@ public synchronized void pauseUpload() { */ public synchronized void continueUpload() { if (mUploadState == STATE_PAUSED) { - Log.d(TAG, "Continuing upload."); + Timber.d("Continuing upload."); mUploadState = STATE_UPLOADING; sendNext(mUploadOffset); } else { - Log.d(TAG, "Upload is not paused."); + Timber.d("Upload is not paused."); } } @@ -482,7 +481,7 @@ private synchronized void failUpload(McuMgrException error) { private synchronized void restartUpload() { if (mImageData == null || mUploadCallback == null) { - Log.e(TAG, "Could not restart upload: image data or callback is null!"); + Timber.e("Could not restart upload: image data or callback is null!"); return; } byte[] tempData = mImageData; @@ -505,7 +504,7 @@ private synchronized void resetUpload() { private synchronized void sendNext(int offset) { // Check that the state is STATE_UPLOADING. if (mUploadState != STATE_UPLOADING) { - Log.d(TAG, "Image Manager is not in the UPLOADING state."); + Timber.d("Image Manager is not in the UPLOADING state."); return; } upload(mImageData, offset, mUploadCallbackImpl); @@ -526,7 +525,7 @@ public void onResponse(@NonNull McuMgrImageUploadResponse response) { // Check for a McuManager error. if (response.rc != 0) { // TODO when the image in slot 1 is confirmed, this will return ENOMEM (2). - Log.e(TAG, "Upload failed due to McuManager error: " + response.rc); + Timber.e("Upload failed due to McuManager error: %s", response.rc); failUpload(new McuMgrErrorException(McuMgrErrorCode.valueOf(response.rc))); return; } @@ -539,7 +538,7 @@ public void onResponse(@NonNull McuMgrImageUploadResponse response) { System.currentTimeMillis()); if (mUploadState == STATE_NONE) { - Log.d(TAG, "Upload canceled!"); + Timber.d("Upload canceled!"); resetUpload(); mUploadCallback.onUploadCanceled(); mUploadCallback = null; @@ -548,7 +547,7 @@ public void onResponse(@NonNull McuMgrImageUploadResponse response) { // Check if the upload has finished. if (mUploadOffset == mImageData.length) { - Log.d(TAG, "Upload finished!"); + Timber.d("Upload finished!"); resetUpload(); mUploadCallback.onUploadFinished(); mUploadCallback = null; @@ -604,7 +603,7 @@ private int calculatePacketOverhead(@NonNull byte[] data, int offset) { return cborData.length + 8 + 2 + 3; } } catch (IOException e) { - Log.e(TAG, "Error while calculating packet overhead", e); + Timber.e(e, "Error while calculating packet overhead"); } return -1; } diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/LogManager.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/LogManager.java index d1a60a2f..532b0d4e 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/LogManager.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/managers/LogManager.java @@ -7,8 +7,6 @@ package io.runtime.mcumgr.managers; -import android.util.Log; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -27,13 +25,13 @@ import io.runtime.mcumgr.response.log.McuMgrModuleListResponse; import io.runtime.mcumgr.response.McuMgrResponse; import io.runtime.mcumgr.util.CBOR; +import timber.log.Timber; /** * Log command group manager. */ @SuppressWarnings({"unused", "WeakerAccess"}) public class LogManager extends McuManager { - private final static String TAG = "LogManager"; // Command IDs private final static int ID_READ = 0; @@ -215,19 +213,19 @@ public synchronized Map<String, State> getAll() { // Get available logs McuMgrLogListResponse logListResponse = logsList(); if (logListResponse == null) { - Log.e(TAG, "Error occurred getting the list of logs."); + Timber.e("Error occurred getting the list of logs."); return logStates; } - Log.d(TAG, "Available logs: " + logListResponse.toString()); + Timber.d("Available logs: %s", logListResponse.toString()); if (logListResponse.log_list == null) { - Log.w(TAG, "No logs available on this device"); + Timber.w("No logs available on this device"); return logStates; } // For each log, get all the available logs for (String logName : logListResponse.log_list) { - Log.d(TAG, "Getting logs for log " + logName); + Timber.d("Getting logs from: %s", logName); // Put a new State mapping if necessary State state = logStates.get(logName); if (state == null) { @@ -239,7 +237,7 @@ public synchronized Map<String, State> getAll() { } return logStates; } catch (McuMgrException e) { - Log.e(TAG, "Transport error while getting available logs", e); + Timber.e(e, "Transport error while getting available logs"); } return logStates; } @@ -261,27 +259,27 @@ public State getAllFromState(State state) { McuMgrLogResponse showResponse = showNext(state); // Check for an error if (showResponse == null) { - Log.e(TAG, "Show logs resulted in an error"); + Timber.e("Show logs resulted in an error"); break; } // // Check for an index mismatch // if (showResponse.next_index < state.getNextIndex()) -// Log.w(TAG, "Next index mismatch state.nextIndex=" + state.getNextIndex() + +// Timber.w("Next index mismatch state.nextIndex=" + state.getNextIndex() + // ", response.nextIndex=" + showResponse.next_index); -// Log.w(TAG, "Resetting log state."); +// Timber.w("Resetting log state."); // state.reset(); // continue; // } // Check that the logs collected are not null or empty if (showResponse.logs == null || showResponse.logs.length == 0) { - Log.e(TAG, "No logs returned in the response."); + Timber.e("No logs returned in the response."); break; } // Get the log result object McuMgrLogResponse.LogResult log = showResponse.logs[0]; // If we don't have any more entries, break out of this log to the next. if (log.entries == null || log.entries.length == 0) { - Log.d(TAG, "No more entries left for this log."); + Timber.d("No more entries left for this log."); break; } // Get the index of the last entry in the list and set the LogState nextIndex @@ -302,20 +300,19 @@ public State getAllFromState(State state) { * @return The show response. */ public McuMgrLogResponse showNext(State state) { - Log.d(TAG, "Show logs: name=" + state.getName() + - ", nextIndex=" + state.getNextIndex()); + Timber.d("Show logs: name=%s, nextIndex=", state.getName(), state.getNextIndex()); try { McuMgrLogResponse response = show(state.getName(), state.getNextIndex(), null); if (response == null) { - Log.e(TAG, "Error occurred getting logs"); + Timber.e("Error occurred getting logs"); return null; } - Log.v(TAG, "Show logs response: " + CBOR.toString(response.getPayload())); + Timber.v("Show logs response: %s", CBOR.toString(response.getPayload())); return response; } catch (McuMgrException e) { - Log.e(TAG, "Requesting next set of logs failed", e); + Timber.e(e, "Requesting next set of logs failed"); } catch (IOException e) { - Log.e(TAG, "Parsing response failed", e); + Timber.e(e, "Parsing response failed"); } return null; diff --git a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/response/McuMgrResponse.java b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/response/McuMgrResponse.java index a654aee5..5a111be7 100644 --- a/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/response/McuMgrResponse.java +++ b/mcumgr-android-lib/src/main/java/io/runtime/mcumgr/response/McuMgrResponse.java @@ -6,8 +6,6 @@ package io.runtime.mcumgr.response; -import android.util.Log; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.io.IOException; @@ -18,11 +16,11 @@ import io.runtime.mcumgr.McuMgrScheme; import io.runtime.mcumgr.exception.McuMgrCoapException; import io.runtime.mcumgr.util.CBOR; +import timber.log.Timber; @SuppressWarnings({"WeakerAccess", "unused"}) @JsonIgnoreProperties(ignoreUnknown = true) public class McuMgrResponse { - private final static String TAG = "McuMgrResponse"; /** * The raw return code found in most McuMgr response payloads. If a rc value is not explicitly @@ -72,7 +70,7 @@ public String toString() { try { return CBOR.toString(mPayload); } catch (IOException e) { - Log.e(TAG, "Failed to parse response", e); + Timber.e(e, "Failed to parse response"); } return null; } @@ -93,7 +91,7 @@ public McuMgrHeader getHeader() { */ public int getReturnCodeValue() { if (mReturnCode == null) { - Log.w(TAG, "Response does not contain a McuMgr return code."); + Timber.w("Response does not contain a McuMgr return code."); return 0; } else { return mReturnCode.value(); @@ -241,7 +239,7 @@ public static <T extends McuMgrResponse> T buildCoapResponse(McuMgrScheme scheme Class<T> type) throws IOException, McuMgrCoapException { // If the code class indicates a CoAP error response, throw a McuMgrCoapException if (codeClass == 4 || codeClass == 5) { - Log.e(TAG, "Received CoAP Error response, throwing McuMgrCoapException"); + Timber.e("Received CoAP Error response, throwing McuMgrCoapException"); throw new McuMgrCoapException(bytes, codeClass, codeDetail); } diff --git a/sample/build.gradle b/sample/build.gradle index df394d19..3df90245 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,13 +7,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 + compileSdkVersion 28 buildToolsVersion "27.0.3" defaultConfig { applicationId "io.runtime.mcumgr" minSdkVersion 21 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName "1.0" resConfigs "en" @@ -29,10 +29,10 @@ android { } dependencies { - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:design:27.1.1' - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' + implementation 'com.android.support:appcompat-v7:28.0.0-rc01' + implementation 'com.android.support:design:28.0.0-rc01' + implementation 'com.android.support:recyclerview-v7:28.0.0-rc01' + implementation 'com.android.support:cardview-v7:28.0.0-rc01' implementation 'com.android.support.constraint:constraint-layout:1.1.2' // Lifecycle extensions @@ -52,6 +52,9 @@ dependencies { // Brings the new BluetoothLeScanner API to older platforms implementation 'no.nordicsemi.android.support.v18:scanner:1.1.0' + // Timber + implementation 'com.jakewharton.timber:timber:4.7.1' + // Mcu Mgr implementation project(path: ':mcumgr-android-lib') diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ad60f7a0..3f12f2f1 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -3,10 +3,29 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <!-- + Bluetooth permission is required in order to communicate with Bluetooth LE devices. + --> <uses-permission android:name="android.permission.BLUETOOTH"/> + + <!-- + Bluetooth Admin permission is required in order to scan for Bluetooth LE devices. + --> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> + <!-- + Location permission is required from Android 6 to be able to scan for advertising + Bluetooth LE devices. Some BLE devices, called beacons, may be used to position the phone. + This is to ensure that the user agrees to do so. + This app does not use this location information in any way. + --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> - <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <!-- + This permission is required to read a file content when the file browser app have returned + file:// URI, instead of content:// URI. This way of passing URIs is deprecated, but still + may be used by some File Browser apps. Since Android 6+ this permission is a runtime type + permission and must be requested on runtime. Check FileBrowserFragment class. + --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:allowBackup="false" diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/application/Dagger2Application.java b/sample/src/main/java/io/runtime/mcumgr/sample/application/Dagger2Application.java index fdffc8fb..dd2b9704 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/application/Dagger2Application.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/application/Dagger2Application.java @@ -17,6 +17,7 @@ import dagger.android.HasActivityInjector; import io.runtime.mcumgr.sample.di.AppInjector; import io.runtime.mcumgr.sample.di.component.McuMgrSubComponent; +import timber.log.Timber; public class Dagger2Application extends Application implements HasActivityInjector { @@ -32,6 +33,9 @@ public void onCreate() { // The app injector makes sure that all activities and fragments that implement Injectable // are injected in onCreate(...) or onActivityCreated(...) AppInjector.init(this); + + // Plant a Timber DebugTree to collect logs from sample app and McuManager + Timber.plant(new Timber.DebugTree()); } @Override diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FileBrowserFragment.java b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FileBrowserFragment.java index 898c7548..dd3a8f61 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FileBrowserFragment.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FileBrowserFragment.java @@ -6,8 +6,10 @@ package io.runtime.mcumgr.sample.fragment.mcumgr; +import android.Manifest; import android.app.Activity; import android.content.Intent; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -15,6 +17,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; @@ -30,17 +33,23 @@ import java.io.InputStream; import io.runtime.mcumgr.sample.R; +import io.runtime.mcumgr.sample.utils.Utils; +import timber.log.Timber; public abstract class FileBrowserFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { private static final String TAG = FileBrowserFragment.class.getSimpleName(); + private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 1023; // random number + private static final int SELECT_FILE_REQ = 1; private static final int LOAD_FILE_LOADER_REQ = 2; private static final String EXTRA_FILE_URI = "uri"; private static final String SIS_DATA = "data"; + private static final String SIS_URI = "uri"; private byte[] mFileContent; + private Uri mFileUri; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -48,6 +57,7 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { if (savedInstanceState != null) { mFileContent = savedInstanceState.getByteArray(SIS_DATA); + mFileUri = savedInstanceState.getParcelable(SIS_URI); } } @@ -55,6 +65,7 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); outState.putByteArray(SIS_DATA, mFileContent); + outState.putParcelable(SIS_URI, mFileUri); } /** @@ -115,6 +126,20 @@ protected boolean isFileLoaded() { */ protected abstract void onFileLoadingFailed(@StringRes final int error); + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + switch (requestCode) { + case REQUEST_WRITE_EXTERNAL_STORAGE: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadFile(mFileUri); + } + mFileUri = null; + break; + } + } + @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -134,24 +159,31 @@ public void onActivityResult(final int requestCode, final int resultCode, final // The URI returned may be of 2 schemes: file:// (legacy) or content:// (new) if (uri.getScheme().equals("file")) { - // TODO This may require WRITE_EXTERNAL_STORAGE permission! - final String path = uri.getPath(); - final String fileName = path.substring(path.lastIndexOf('/')); - - final File file = new File(path); - final int fileSize = (int) file.length(); - onFileSelected(fileName, fileSize); - try { - loadContent(new FileInputStream(file)); - } catch (final FileNotFoundException e) { - Log.e(TAG, "File not found", e); - onFileLoadingFailed(R.string.file_loader_error_no_uri); + if (Utils.isStoragePermissionsGranted(requireContext())) { + loadFile(uri); + } else { + if (Utils.isStoragePermissionDeniedForever(requireActivity())) { + Snackbar.make(getView(), R.string.file_loader_permission_denied, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings, v -> { + final Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null)); + startActivity(intent); + }) + .show(); + return; + } + mFileUri = uri; + Utils.markStoragePermissionRequested(requireContext()); + requestPermissions( + new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, + REQUEST_WRITE_EXTERNAL_STORAGE + ); } } else { // File name and size must be obtained from Content Provider final Bundle bundle = new Bundle(); bundle.putParcelable(EXTRA_FILE_URI, uri); - getLoaderManager().restartLoader(LOAD_FILE_LOADER_REQ, bundle, this); + LoaderManager.getInstance(this).restartLoader(LOAD_FILE_LOADER_REQ, bundle, this); } } } @@ -206,17 +238,17 @@ public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor da .openInputStream(cursorLoader.getUri()); loadContent(is); } catch (final FileNotFoundException e) { - Log.e(TAG, "File not found", e); + Timber.e(e, "File not found"); onFileLoadingFailed(R.string.file_loader_error_no_uri); } } else { - Log.e(TAG, "Empty cursor"); + Timber.e("Empty cursor"); onFileLoadingFailed(R.string.file_loader_error_no_uri); } // Reset the loader as the URU read permission is one time only. // We keep the file content in the fragment so no need to load it again. // onLoaderReset(...) will be called after that. - getLoaderManager().destroyLoader(LOAD_FILE_LOADER_REQ); + LoaderManager.getInstance(this).destroyLoader(LOAD_FILE_LOADER_REQ); } } } @@ -244,6 +276,27 @@ protected void selectFile(@Nullable final String mimeType) { } } + /** + * Loads file given in file:// scheme. This will not work with content:// scheme. + * The app must have WRITE_EXTERNAL_STORAGE permission in order to read the file. + * + * @param uri the file URI in file:// scheme. + */ + private void loadFile(@NonNull final Uri uri) { + final String path = uri.getPath(); + final String fileName = path.substring(path.lastIndexOf('/') + 1); + + final File file = new File(path); + final int fileSize = (int) file.length(); + onFileSelected(fileName, fileSize); + try { + loadContent(new FileInputStream(file)); + } catch (final FileNotFoundException e) { + Timber.e(e, "File not found"); + onFileLoadingFailed(R.string.file_loader_error_no_uri); + } + } + /** * Loads content from the stream. * @@ -272,7 +325,7 @@ private void loadContent(@Nullable final InputStream is) { mFileContent = bytes; onFileLoaded(bytes); } catch (final IOException e) { - Log.e(TAG, "Reading file content failed", e); + Timber.e(e, "Reading file content failed"); onFileLoadingFailed(R.string.file_loader_error_loading_file_failed); } } diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/utils/Utils.java b/sample/src/main/java/io/runtime/mcumgr/sample/utils/Utils.java index 2e57ac8c..b4b1ea62 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/utils/Utils.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/utils/Utils.java @@ -21,10 +21,12 @@ public class Utils { private static final String PREFS_LOCATION_NOT_REQUIRED = "location_not_required"; private static final String PREFS_PERMISSION_REQUESTED = "permission_requested"; + private static final String PREFS_STORAGE_PERMISSION_REQUESTED = "storage_permission_requested"; /** * Checks whether Bluetooth is enabled. - * @return true if Bluetooth is enabled, false otherwise. + * + * @return True if Bluetooth is enabled, false otherwise. */ public static boolean isBleEnabled() { final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -34,17 +36,59 @@ public static boolean isBleEnabled() { /** * Checks for required permissions. * - * @return true if permissions are already granted, false otherwise. + * @return True if permissions are already granted, false otherwise. + */ + public static boolean isStoragePermissionsGranted(final Context context) { + return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Returns true if storage permission has been requested at least twice and + * user denied it, and checked 'Don't ask again'. + * + * @param activity the activity. + * @return True if permission has been denied and the popup will not come up any more, + * false otherwise. + */ + public static boolean isStoragePermissionDeniedForever(final Activity activity) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + + return !isStoragePermissionsGranted(activity) // Storage permission must be denied + && preferences.getBoolean(PREFS_STORAGE_PERMISSION_REQUESTED, false) // Permission must have been requested before + && !ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); // This method should return false + } + + /** + * The first time an app requests a permission there is no 'Don't ask again' checkbox and + * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)} returns false. + * This situation is similar to a permission being denied forever, so to distinguish both cases + * a flag needs to be saved. + * + * @param context the context. + */ + public static void markStoragePermissionRequested(final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit().putBoolean(PREFS_STORAGE_PERMISSION_REQUESTED, true).apply(); + } + + /** + * Checks for required permissions. + * + * @return True if permissions are already granted, false otherwise. */ public static boolean isLocationPermissionsGranted(final Context context) { - return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED; } /** * Returns true if location permission has been requested at least twice and * user denied it, and checked 'Don't ask again'. - * @param activity the activity - * @return true if permission has been denied and the popup will not come up any more, false otherwise + * + * @param activity the activity. + * @return True if permission has been denied and the popup will not come up any more, + * false otherwise. */ public static boolean isLocationPermissionDeniedForever(final Activity activity) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); @@ -55,10 +99,12 @@ public static boolean isLocationPermissionDeniedForever(final Activity activity) } /** - * On some devices running Android Marshmallow or newer location services must be enabled in order to scan for Bluetooth LE devices. + * On some devices running Android Marshmallow or newer location services must be enabled in + * order to scan for Bluetooth LE devices. * This method returns whether the Location has been enabled or not. * - * @return true on Android 6.0+ if location mode is different than LOCATION_MODE_OFF. It always returns true on Android versions prior to Marshmallow. + * @return true on Android 6.0+ if location mode is different than LOCATION_MODE_OFF. + * It always returns true on Android versions prior to Marshmallow. */ public static boolean isLocationEnabled(final Context context) { if (isMarshmallowOrAbove()) { @@ -74,10 +120,11 @@ public static boolean isLocationEnabled(final Context context) { } /** - * Location enabled is required on some phones running Android Marshmallow or newer (for example on Nexus and Pixel devices). + * Location enabled is required on some phones running Android Marshmallow or newer + * (for example on Nexus and Pixel devices). * - * @param context the context - * @return false if it is known that location is not required, true otherwise + * @param context the context. + * @return False if it is known that location is not required, true otherwise. */ public static boolean isLocationRequired(final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -86,9 +133,11 @@ public static boolean isLocationRequired(final Context context) { /** * When a Bluetooth LE packet is received while Location is disabled it means that Location - * is not required on this device in order to scan for LE devices. This is a case of Samsung phones, for example. - * Save this information for the future to keep the Location info hidden. - * @param context the context + * is not required on this device in order to scan for LE devices. + * This is a case of Samsung phones, for example. Save this information for the future to + * keep the Location info hidden. + * + * @param context the context. */ public static void markLocationNotRequired(final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -100,7 +149,8 @@ public static void markLocationNotRequired(final Context context) { * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)} returns false. * This situation is similar to a permission being denied forever, so to distinguish both cases * a flag needs to be saved. - * @param context the context + * + * @param context the context. */ public static void markLocationPermissionRequested(final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/SingleLiveEvent.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/SingleLiveEvent.java index dda36fd1..92c2f763 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/SingleLiveEvent.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/SingleLiveEvent.java @@ -12,10 +12,11 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import java.util.concurrent.atomic.AtomicBoolean; +import timber.log.Timber; + /** * A lifecycle-aware observable that sends only new updates after subscription, used for events like * navigation and Snackbar messages. @@ -28,7 +29,6 @@ */ @SuppressWarnings("unused") public class SingleLiveEvent<T> extends MutableLiveData<T> { - private static final String TAG = "SingleLiveEvent"; private final AtomicBoolean mPending = new AtomicBoolean(false); @@ -36,7 +36,7 @@ public class SingleLiveEvent<T> extends MutableLiveData<T> { public void observe(@NonNull final LifecycleOwner owner, @NonNull final Observer<T> observer) { if (hasActiveObservers()) { - Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + Timber.w("Multiple observers registered but only one will be notified of changes."); } // Observe the internal MutableLiveData diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/FilesDownloadViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/FilesDownloadViewModel.java index 91b3015a..16c4a268 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/FilesDownloadViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/FilesDownloadViewModel.java @@ -99,7 +99,6 @@ public void onDownloadFailed(@NonNull final McuMgrException error) { if (error instanceof McuMgrErrorException) { final McuMgrErrorCode code = ((McuMgrErrorException) error).getCode(); if (code == McuMgrErrorCode.UNKNOWN) { - // TODO Verify mResponseLiveData.postValue(null); // File not found postReady(); return; diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java index eaa82da6..679af158 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java @@ -162,10 +162,11 @@ private void postReady(@Nullable final McuMgrImageStateResponse response) { && response.images != null && response.images.length > 1; final boolean slot1NotPending = hasSlot1 && !response.images[1].pending; final boolean slot1NotPermanent = hasSlot1 && !response.images[1].permanent; + final boolean slot1NotConfirmed = hasSlot1 && !response.images[1].confirmed; mResponseLiveData.postValue(response); mTestAvailableLiveData.postValue(slot1NotPending); mConfirmAvailableLiveData.postValue(slot1NotPermanent); - mEraseAvailableLiveData.postValue(hasSlot1); + mEraseAvailableLiveData.postValue(slot1NotConfirmed); postReady(); } } diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java index 32a87469..c1a82426 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java @@ -39,7 +39,7 @@ public boolean canPauseOrResume() { } public boolean canCancel() { - return this == UPLOADING || this == PAUSED; + return this == VALIDATING || this == UPLOADING || this == PAUSED; } } diff --git a/sample/src/main/res/layout/dialog_files_settings.xml b/sample/src/main/res/layout/dialog_files_settings.xml index ef85011f..dfa218b9 100644 --- a/sample/src/main/res/layout/dialog_files_settings.xml +++ b/sample/src/main/res/layout/dialog_files_settings.xml @@ -20,6 +20,8 @@ <android.support.design.widget.TextInputEditText android:id="@+id/partition" android:layout_width="match_parent" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content"> + <requestFocus/> + </android.support.design.widget.TextInputEditText> </android.support.design.widget.TextInputLayout> </LinearLayout> \ No newline at end of file diff --git a/sample/src/main/res/layout/dialog_generate_file.xml b/sample/src/main/res/layout/dialog_generate_file.xml index 4f4208b1..dd5e70fc 100644 --- a/sample/src/main/res/layout/dialog_generate_file.xml +++ b/sample/src/main/res/layout/dialog_generate_file.xml @@ -24,7 +24,9 @@ android:id="@+id/file_size" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="number"/> + android:inputType="number"> + <requestFocus/> + </android.support.design.widget.TextInputEditText> </android.support.design.widget.TextInputLayout> <TextView diff --git a/sample/src/main/res/layout/fragment_card_device_status.xml b/sample/src/main/res/layout/fragment_card_device_status.xml index c84f4b98..89f1654d 100644 --- a/sample/src/main/res/layout/fragment_card_device_status.xml +++ b/sample/src/main/res/layout/fragment_card_device_status.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -79,4 +79,4 @@ app:layout_constraintTop_toBottomOf="@+id/connection_status_label"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_echo.xml b/sample/src/main/res/layout/fragment_card_echo.xml index ecc136d0..12cb9c9b 100644 --- a/sample/src/main/res/layout/fragment_card_echo.xml +++ b/sample/src/main/res/layout/fragment_card_echo.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -40,9 +40,9 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/toolbar"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_send" - style="@style/Widget.AppCompat.Button.Colored" + style="@style/Widget.MaterialComponents.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" @@ -99,4 +99,4 @@ </LinearLayout> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_files_download.xml b/sample/src/main/res/layout/fragment_card_files_download.xml index cab4e340..6a7f8d83 100644 --- a/sample/src/main/res/layout/fragment_card_files_download.xml +++ b/sample/src/main/res/layout/fragment_card_files_download.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -62,9 +62,9 @@ app:layout_constraintTop_toBottomOf="@+id/file_name" tools:text="/nffs/file"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_download" - style="@style/Widget.AppCompat.Button.Colored" + style="@style/Widget.MaterialComponents.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" @@ -126,4 +126,4 @@ tools:visibility="visible"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_files_upload.xml b/sample/src/main/res/layout/fragment_card_files_upload.xml index daa1e88b..b92b6b82 100644 --- a/sample/src/main/res/layout/fragment_card_files_upload.xml +++ b/sample/src/main/res/layout/fragment_card_files_upload.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -128,7 +128,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/progress"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_generate" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -139,7 +139,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_select_file" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -149,7 +149,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_upload" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_upload" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -161,7 +161,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_cancel" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_cancel" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -173,7 +173,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_pause_resume" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_pause_resume" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -186,4 +186,4 @@ app:layout_constraintTop_toBottomOf="@+id/divider"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_image_control.xml b/sample/src/main/res/layout/fragment_card_image_control.xml index 7c1cc92b..62dc8f1e 100644 --- a/sample/src/main/res/layout/fragment_card_image_control.xml +++ b/sample/src/main/res/layout/fragment_card_image_control.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -63,7 +63,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/image_control_error"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_read" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -73,7 +73,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_test" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_test" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -84,7 +84,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_confirm" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_confirm" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -95,7 +95,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_erase" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_erase" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -109,4 +109,4 @@ app:layout_constraintTop_toBottomOf="@+id/divider"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_image_upgrade.xml b/sample/src/main/res/layout/fragment_card_image_upgrade.xml index 84d2d0f6..801399d7 100644 --- a/sample/src/main/res/layout/fragment_card_image_upgrade.xml +++ b/sample/src/main/res/layout/fragment_card_image_upgrade.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -125,7 +125,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/progress"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_select_file" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -135,7 +135,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_start" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_start" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -147,7 +147,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_cancel" app:layout_constraintTop_toTopOf="@+id/action_select_file"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_cancel" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -159,7 +159,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_pause_resume" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_pause_resume" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -172,4 +172,4 @@ app:layout_constraintTop_toBottomOf="@+id/divider"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_image_upload.xml b/sample/src/main/res/layout/fragment_card_image_upload.xml index fd2915fa..a06473ac 100644 --- a/sample/src/main/res/layout/fragment_card_image_upload.xml +++ b/sample/src/main/res/layout/fragment_card_image_upload.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -125,7 +125,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/progress"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_select_file" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -135,7 +135,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_upload" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_upload" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -147,7 +147,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_cancel" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_cancel" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -159,7 +159,7 @@ app:layout_constraintEnd_toStartOf="@+id/action_pause_resume" app:layout_constraintTop_toBottomOf="@+id/divider"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_pause_resume" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -172,4 +172,4 @@ app:layout_constraintTop_toBottomOf="@+id/divider"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_reset.xml b/sample/src/main/res/layout/fragment_card_reset.xml index 511363ba..6e4a0e2d 100644 --- a/sample/src/main/res/layout/fragment_card_reset.xml +++ b/sample/src/main/res/layout/fragment_card_reset.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -28,7 +28,7 @@ <android.support.v7.widget.AppCompatButton android:id="@+id/action_reset" - style="@style/Widget.AppCompat.Button.Colored" + style="@style/Widget.MaterialComponents.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" @@ -54,4 +54,4 @@ tools:text="Error: Not supported"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_card_stats.xml b/sample/src/main/res/layout/fragment_card_stats.xml index 02026b08..d90655a7 100644 --- a/sample/src/main/res/layout/fragment_card_stats.xml +++ b/sample/src/main/res/layout/fragment_card_stats.xml @@ -5,7 +5,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<android.support.v7.widget.CardView +<android.support.design.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -63,7 +63,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/image_control_error"/> - <Button + <android.support.design.button.MaterialButton android:id="@+id/action_refresh" style="@style/Widget.ActionButton" android:layout_width="wrap_content" @@ -75,4 +75,4 @@ app:layout_constraintTop_toBottomOf="@+id/divider"/> </android.support.constraint.ConstraintLayout> -</android.support.v7.widget.CardView> \ No newline at end of file +</android.support.design.card.MaterialCardView> \ No newline at end of file diff --git a/sample/src/main/res/values-w360dp/strings_files_upload.xml b/sample/src/main/res/values-w380dp/strings_files_upload.xml similarity index 100% rename from sample/src/main/res/values-w360dp/strings_files_upload.xml rename to sample/src/main/res/values-w380dp/strings_files_upload.xml diff --git a/sample/src/main/res/values-w380dp/styles.xml b/sample/src/main/res/values-w380dp/styles.xml index 352f5197..315cc678 100644 --- a/sample/src/main/res/values-w380dp/styles.xml +++ b/sample/src/main/res/values-w380dp/styles.xml @@ -7,7 +7,7 @@ <resources> - <style name="Widget.ActionButton" parent="Widget.AppCompat.Button.Borderless.Colored"> + <style name="Widget.ActionButton" parent="Widget.MaterialComponents.Button.TextButton"> <!-- Empty--> </style> diff --git a/sample/src/main/res/values/strings_file_loader.xml b/sample/src/main/res/values/strings_file_loader.xml index 80ffc6b3..076eee76 100644 --- a/sample/src/main/res/values/strings_file_loader.xml +++ b/sample/src/main/res/values/strings_file_loader.xml @@ -9,4 +9,5 @@ <string name="file_loader_error_no_file_browser">File Browser app not found.</string> <string name="file_loader_error_no_uri">No file found.</string> <string name="file_loader_error_loading_file_failed">Reading file failed.</string> + <string name="file_loader_permission_denied">Storage permission is denied.</string> </resources> \ No newline at end of file diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml index 0970d453..44e307d5 100644 --- a/sample/src/main/res/values/styles.xml +++ b/sample/src/main/res/values/styles.xml @@ -13,7 +13,7 @@ <item name="android:textStyle">bold</item> </style> - <style name="Widget.ActionButton" parent="Widget.AppCompat.Button.Borderless.Colored"> + <style name="Widget.ActionButton" parent="Widget.MaterialComponents.Button.TextButton"> <!-- 4 action buttons don't fit on the screen on narrow phones. Default width would be 4 * 88 dip = 352. For narrower phones, use narrower buttons. @@ -26,7 +26,7 @@ <!-- Customize your theme here. --> </style> - <style name="AppTheme.Base" parent="Theme.AppCompat.Light.NoActionBar"> + <style name="AppTheme.Base" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:windowBackground">@color/background</item> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> @@ -34,8 +34,8 @@ </style> <!-- Main app's toolbar theme. --> - <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/> + <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar"/> - <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/> + <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light"/> </resources>