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>