diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java
index e65488fff..928dbd557 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java
@@ -26,6 +26,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
@@ -37,6 +38,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
+import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.LoggingFragment;
@@ -216,10 +218,36 @@ public final class RestoreBackupFragment extends LoggingFragment {
@NonNull Uri backupUri,
@NonNull OnBackupSearchResultListener listener)
{
- SimpleTask.run(() -> BackupUtil.getBackupInfoFromSingleUri(context, backupUri),
+ SimpleTask.run(() -> {
+ try {
+ return BackupUtil.getBackupInfoFromSingleUri(context, backupUri);
+ } catch (BackupUtil.BackupFileException e) {
+ Log.w(TAG, "Could not restore backup.", e);
+ postToastForBackupRestorationFailure(context, e);
+ return null;
+ }
+ },
listener::run);
}
+ private static void postToastForBackupRestorationFailure(@NonNull Context context, @NonNull BackupUtil.BackupFileException exception) {
+ final @StringRes int errorResId;
+ switch (exception.getState()) {
+ case READABLE:
+ throw new AssertionError("Unexpected error state.");
+ case NOT_FOUND:
+ errorResId = R.string.RestoreBackupFragment__backup_not_found;
+ break;
+ case UNSUPPORTED_FILE_EXTENSION:
+ errorResId = R.string.RestoreBackupFragment__backup_has_a_bad_extension;
+ break;
+ default:
+ errorResId = R.string.RestoreBackupFragment__backup_could_not_be_read;
+ }
+
+ ThreadUtil.postToMain(() -> Toast.makeText(context, errorResId, Toast.LENGTH_LONG).show());
+ }
+
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
index 147d009f4..9b78f6f95 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java
@@ -6,11 +6,11 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
-import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
import androidx.documentfile.provider.DocumentFile;
import org.signal.core.util.logging.Log;
@@ -166,14 +166,22 @@ public class BackupUtil {
return backups;
}
- public static @Nullable BackupInfo getBackupInfoFromSingleUri(@NonNull Context context, @NonNull Uri singleUri) {
- DocumentFile documentFile = DocumentFile.fromSingleUri(context, singleUri);
+ public static @Nullable BackupInfo getBackupInfoFromSingleUri(@NonNull Context context, @NonNull Uri singleUri) throws BackupFileException {
+ DocumentFile documentFile = Objects.requireNonNull(DocumentFile.fromSingleUri(context, singleUri));
- if (isBackupFileReadable(documentFile)) {
+ return getBackupInfoFromSingleDocumentFile(documentFile);
+ }
+
+ @VisibleForTesting
+ static @Nullable BackupInfo getBackupInfoFromSingleDocumentFile(@NonNull DocumentFile documentFile) throws BackupFileException {
+ BackupFileState backupFileState = getBackupFileState(documentFile);
+
+ if (backupFileState.isSuccess()) {
long backupTimestamp = getBackupTimestamp(Objects.requireNonNull(documentFile.getName()));
return new BackupInfo(backupTimestamp, documentFile.length(), documentFile.getUri());
} else {
Log.w(TAG, "Could not load backup info.");
+ backupFileState.throwIfError();
return null;
}
}
@@ -258,21 +266,58 @@ public class BackupUtil {
return -1;
}
- private static boolean isBackupFileReadable(@Nullable DocumentFile documentFile) {
- if (documentFile == null) {
- throw new AssertionError("We do not support platforms prior to KitKat.");
- } else if (!documentFile.exists()) {
- Log.w(TAG, "isBackupFileReadable: The document at the specified Uri cannot be found.");
- return false;
+ private static BackupFileState getBackupFileState(@NonNull DocumentFile documentFile) {
+ if (!documentFile.exists()) {
+ return BackupFileState.NOT_FOUND;
} else if (!documentFile.canRead()) {
- Log.w(TAG, "isBackupFileReadable: The document at the specified Uri cannot be read.");
- return false;
- } else if (TextUtils.isEmpty(documentFile.getName()) || !documentFile.getName().endsWith(".backup")) {
- Log.w(TAG, "isBackupFileReadable: The document at the specified Uri has an unsupported file extension.");
- return false;
+ return BackupFileState.NOT_READABLE;
+ } else if (Util.isEmpty(documentFile.getName()) || !documentFile.getName().endsWith(".backup")) {
+ return BackupFileState.UNSUPPORTED_FILE_EXTENSION;
} else {
- Log.i(TAG, "isBackupFileReadable: The document at the specified Uri looks like a readable backup");
- return true;
+ return BackupFileState.READABLE;
+ }
+ }
+
+ /**
+ * Describes the validity of a backup file.
+ */
+ public enum BackupFileState {
+ READABLE("The document at the specified Uri looks like a readable backup."),
+ NOT_FOUND("The document at the specified Uri cannot be found."),
+ NOT_READABLE("The document at the specified Uri cannot be read."),
+ UNSUPPORTED_FILE_EXTENSION("The document at the specified Uri has an unsupported file extension.");
+
+ private final String message;
+
+ BackupFileState(String message) {
+ this.message = message;
+ }
+
+ public boolean isSuccess() {
+ return this == READABLE;
+ }
+
+ public void throwIfError() throws BackupFileException {
+ if (!isSuccess()) {
+ throw new BackupFileException(this);
+ }
+ }
+ }
+
+ /**
+ * Wrapping exception for a non-successful BackupFileState.
+ */
+ public static class BackupFileException extends Exception {
+
+ private final BackupFileState state;
+
+ BackupFileException(BackupFileState backupFileState) {
+ super(backupFileState.message);
+ this.state = backupFileState;
+ }
+
+ public @NonNull BackupFileState getState() {
+ return state;
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3a6f6408d..6dc3905f9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -471,6 +471,12 @@
To continue using backups, please choose a folder. New backups will be saved to this location.
Choose folder
Not now
+
+ Backup not found.
+
+ Backup could not be read.
+
+ Backup has a bad extension.
Chat backups
diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/BackupUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/BackupUtilTest.kt
new file mode 100644
index 000000000..2a344afc3
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/util/BackupUtilTest.kt
@@ -0,0 +1,84 @@
+package org.thoughtcrime.securesms.util
+
+import androidx.documentfile.provider.DocumentFile
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.BeforeClass
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.testutil.EmptyLogger
+
+class BackupUtilTest {
+
+ companion object {
+ private const val TEST_NAME = "1920837192.backup"
+
+ @BeforeClass
+ @JvmStatic
+ fun setUpClass() {
+ Log.initialize(EmptyLogger())
+ }
+ }
+
+ private val documentFile = mock(DocumentFile::class.java)
+
+ @Test
+ fun `Given a non-existent uri, when I getBackupInfoFromSingleDocumentFile, then I expect NOT_FOUND`() {
+ try {
+ BackupUtil.getBackupInfoFromSingleDocumentFile(documentFile)
+ fail("Expected a BackupFileException")
+ } catch (e: BackupUtil.BackupFileException) {
+ assertEquals(BackupUtil.BackupFileState.NOT_FOUND, e.state)
+ }
+ }
+
+ @Test
+ fun `Given an existent but unreadable uri, when I getBackupInfoFromSingleDocumentFile, then I expect NOT_READABLE`() {
+ givenFileExists()
+
+ try {
+ BackupUtil.getBackupInfoFromSingleDocumentFile(documentFile)
+ fail("Expected a BackupFileException")
+ } catch (e: BackupUtil.BackupFileException) {
+ assertEquals(BackupUtil.BackupFileState.NOT_READABLE, e.state)
+ }
+ }
+
+ @Test
+ fun `Given an existent readable uri with a bad extension, when I getBackupInfoFromSingleDocumentFile, then I expect UNSUPPORTED_FILE_EXTENSION`() {
+ givenFileExists()
+ givenFileIsReadable()
+
+ try {
+ BackupUtil.getBackupInfoFromSingleDocumentFile(documentFile)
+ fail("Expected a BackupFileException")
+ } catch (e: BackupUtil.BackupFileException) {
+ assertEquals(BackupUtil.BackupFileState.UNSUPPORTED_FILE_EXTENSION, e.state)
+ }
+ }
+
+ @Test
+ fun `Given an existent readable uri, when I getBackupInfoFromSingleDocumentFile, then I expect an info`() {
+ givenFileExists()
+ givenFileIsReadable()
+ givenFileHasCorrectExtension()
+
+ val info = BackupUtil.getBackupInfoFromSingleDocumentFile(documentFile)
+ assertNotNull(info)
+ }
+
+ private fun givenFileExists() {
+ doReturn(true).`when`(documentFile).exists()
+ }
+
+ private fun givenFileIsReadable() {
+ doReturn(true).`when`(documentFile).canRead()
+ }
+
+ private fun givenFileHasCorrectExtension() {
+ doReturn(TEST_NAME).`when`(documentFile).name
+ }
+}