kopia lustrzana https://github.com/ryukoposting/Signal-Android
rodzic
80598814bd
commit
63dab3f4b0
|
@ -26,6 +26,7 @@ import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
@ -37,6 +38,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.AppInitialization;
|
import org.thoughtcrime.securesms.AppInitialization;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
|
@ -216,10 +218,36 @@ public final class RestoreBackupFragment extends LoggingFragment {
|
||||||
@NonNull Uri backupUri,
|
@NonNull Uri backupUri,
|
||||||
@NonNull OnBackupSearchResultListener listener)
|
@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);
|
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) {
|
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
|
||||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||||
|
|
|
@ -6,11 +6,11 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
@ -166,14 +166,22 @@ public class BackupUtil {
|
||||||
return backups;
|
return backups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable BackupInfo getBackupInfoFromSingleUri(@NonNull Context context, @NonNull Uri singleUri) {
|
public static @Nullable BackupInfo getBackupInfoFromSingleUri(@NonNull Context context, @NonNull Uri singleUri) throws BackupFileException {
|
||||||
DocumentFile documentFile = DocumentFile.fromSingleUri(context, singleUri);
|
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()));
|
long backupTimestamp = getBackupTimestamp(Objects.requireNonNull(documentFile.getName()));
|
||||||
return new BackupInfo(backupTimestamp, documentFile.length(), documentFile.getUri());
|
return new BackupInfo(backupTimestamp, documentFile.length(), documentFile.getUri());
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Could not load backup info.");
|
Log.w(TAG, "Could not load backup info.");
|
||||||
|
backupFileState.throwIfError();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,21 +266,58 @@ public class BackupUtil {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isBackupFileReadable(@Nullable DocumentFile documentFile) {
|
private static BackupFileState getBackupFileState(@NonNull DocumentFile documentFile) {
|
||||||
if (documentFile == null) {
|
if (!documentFile.exists()) {
|
||||||
throw new AssertionError("We do not support platforms prior to KitKat.");
|
return BackupFileState.NOT_FOUND;
|
||||||
} else if (!documentFile.exists()) {
|
|
||||||
Log.w(TAG, "isBackupFileReadable: The document at the specified Uri cannot be found.");
|
|
||||||
return false;
|
|
||||||
} else if (!documentFile.canRead()) {
|
} else if (!documentFile.canRead()) {
|
||||||
Log.w(TAG, "isBackupFileReadable: The document at the specified Uri cannot be read.");
|
return BackupFileState.NOT_READABLE;
|
||||||
return false;
|
} else if (Util.isEmpty(documentFile.getName()) || !documentFile.getName().endsWith(".backup")) {
|
||||||
} else if (TextUtils.isEmpty(documentFile.getName()) || !documentFile.getName().endsWith(".backup")) {
|
return BackupFileState.UNSUPPORTED_FILE_EXTENSION;
|
||||||
Log.w(TAG, "isBackupFileReadable: The document at the specified Uri has an unsupported file extension.");
|
|
||||||
return false;
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "isBackupFileReadable: The document at the specified Uri looks like a readable backup");
|
return BackupFileState.READABLE;
|
||||||
return true;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -471,6 +471,12 @@
|
||||||
<string name="RestoreBackupFragment__to_continue_using_backups_please_choose_a_folder">To continue using backups, please choose a folder. New backups will be saved to this location.</string>
|
<string name="RestoreBackupFragment__to_continue_using_backups_please_choose_a_folder">To continue using backups, please choose a folder. New backups will be saved to this location.</string>
|
||||||
<string name="RestoreBackupFragment__choose_folder">Choose folder</string>
|
<string name="RestoreBackupFragment__choose_folder">Choose folder</string>
|
||||||
<string name="RestoreBackupFragment__not_now">Not now</string>
|
<string name="RestoreBackupFragment__not_now">Not now</string>
|
||||||
|
<!-- Couldn't find the selected backup -->
|
||||||
|
<string name="RestoreBackupFragment__backup_not_found">Backup not found.</string>
|
||||||
|
<!-- Couldn't read the selected backup -->
|
||||||
|
<string name="RestoreBackupFragment__backup_could_not_be_read">Backup could not be read.</string>
|
||||||
|
<!-- Backup has an unsupported file extension -->
|
||||||
|
<string name="RestoreBackupFragment__backup_has_a_bad_extension">Backup has a bad extension.</string>
|
||||||
|
|
||||||
<!-- BackupsPreferenceFragment -->
|
<!-- BackupsPreferenceFragment -->
|
||||||
<string name="BackupsPreferenceFragment__chat_backups">Chat backups</string>
|
<string name="BackupsPreferenceFragment__chat_backups">Chat backups</string>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue