diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java index ea0bc6ed6..5e4f0fc10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupFileIOError.java @@ -18,10 +18,10 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels; import java.io.IOException; public enum BackupFileIOError { - ACCESS_ERROR(R.string.LocalBackupJobApi29_backups_disabled, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved), + ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved), FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large), NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space), - UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_backup_failed_for_an_unknown_reason); + UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups); private static final short BACKUP_FAILED_ID = 31321; @@ -42,8 +42,8 @@ public enum BackupFileIOError { intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true); - PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0); - Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.BACKUPS) + PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0); + Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES) .setSmallIcon(R.drawable.ic_signal_backup) .setContentTitle(context.getString(titleId)) .setContentText(context.getString(messageId)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index c11a5f3f2..145bb4e43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigra import org.thoughtcrime.securesms.migrations.AttributesMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarMigrationJob; +import org.thoughtcrime.securesms.migrations.BackupNotificationMigrationJob; import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob; import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob; import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob; @@ -135,6 +136,7 @@ public final class JobManagerFactories { put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory()); put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory()); put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory()); + put(BackupNotificationMigrationJob.KEY, new BackupNotificationMigrationJob.Factory()); put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory()); put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 622793eb2..7da12d744 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.jobs; import android.Manifest; -import android.content.Context; import androidx.annotation.NonNull; @@ -94,7 +93,7 @@ public final class LocalBackupJob extends BaseJob { notification.setIndeterminateProgress(); String backupPassword = BackupPassphrase.get(context); - File backupDirectory = StorageUtil.getBackupDirectory(); + File backupDirectory = StorageUtil.getOrCreateBackupDirectory(); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date()); String fileName = String.format("signal-%s.backup", timestamp); File backupFile = new File(backupDirectory, fileName); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java index 2f384ba50..741e48345 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java @@ -89,7 +89,6 @@ public final class LocalBackupJobApi29 extends BaseJob { String fileName = String.format("signal-%s.backup", timestamp); if (backupDirectory == null || !backupDirectory.canWrite()) { - BackupUtil.disableBackups(context); BackupFileIOError.ACCESS_ERROR.postNotification(context); throw new IOException("Cannot write to backup directory location."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 21e1881f6..a1c652f0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -39,31 +39,32 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 22; + public static final int CURRENT_VERSION = 23; private static final class Version { - static final int LEGACY = 1; - static final int RECIPIENT_ID = 2; - static final int RECIPIENT_SEARCH = 3; - static final int RECIPIENT_CLEANUP = 4; - static final int AVATAR_MIGRATION = 5; - static final int UUIDS = 6; - static final int CACHED_ATTACHMENTS = 7; - static final int STICKERS_LAUNCH = 8; + static final int LEGACY = 1; + static final int RECIPIENT_ID = 2; + static final int RECIPIENT_SEARCH = 3; + static final int RECIPIENT_CLEANUP = 4; + static final int AVATAR_MIGRATION = 5; + static final int UUIDS = 6; + static final int CACHED_ATTACHMENTS = 7; + static final int STICKERS_LAUNCH = 8; //static final int TEST_ARGON2 = 9; - static final int SWOON_STICKERS = 10; - static final int STORAGE_SERVICE = 11; + static final int SWOON_STICKERS = 10; + static final int STORAGE_SERVICE = 11; //static final int STORAGE_KEY_ROTATE = 12; - static final int REMOVE_AVATAR_ID = 13; - static final int STORAGE_CAPABILITY = 14; - static final int PIN_REMINDER = 15; - static final int VERSIONED_PROFILE = 16; - static final int PIN_OPT_OUT = 17; - static final int TRIM_SETTINGS = 18; - static final int THUMBNAIL_CLEANUP = 19; - static final int GV2 = 20; - static final int GV2_2 = 21; - static final int CDS = 22; + static final int REMOVE_AVATAR_ID = 13; + static final int STORAGE_CAPABILITY = 14; + static final int PIN_REMINDER = 15; + static final int VERSIONED_PROFILE = 16; + static final int PIN_OPT_OUT = 17; + static final int TRIM_SETTINGS = 18; + static final int THUMBNAIL_CLEANUP = 19; + static final int GV2 = 20; + static final int GV2_2 = 21; + static final int CDS = 22; + static final int BACKUP_NOTIFICATION = 23; } /** @@ -267,6 +268,10 @@ public class ApplicationMigrations { jobs.put(Version.CDS, new DirectoryRefreshMigrationJob()); } + if (lastSeenVersion < Version.BACKUP_NOTIFICATION) { + jobs.put(Version.BACKUP_NOTIFICATION, new BackupNotificationMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupNotificationMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupNotificationMigrationJob.java new file mode 100644 index 000000000..a90cec61d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupNotificationMigrationJob.java @@ -0,0 +1,68 @@ +package org.thoughtcrime.securesms.migrations; + +import android.os.Build; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.backup.BackupFileIOError; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.BackupUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +import java.io.IOException; +import java.util.Locale; + +/** + * Handles showing a notification if we think backups were unintentionally disabled. + */ +public final class BackupNotificationMigrationJob extends MigrationJob { + + private static final String TAG = Log.tag(BackupNotificationMigrationJob.class); + + public static final String KEY = "BackupNotificationMigrationJob"; + + BackupNotificationMigrationJob() { + this(new Parameters.Builder().build()); + } + + private BackupNotificationMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + if (Build.VERSION.SDK_INT >= 29 && !TextSecurePreferences.isBackupEnabled(context) && BackupUtil.hasBackupFiles(context)) { + Log.w(TAG, "Stranded backup! Notifying."); + BackupFileIOError.UNKNOWN.postNotification(context); + } else { + Log.w(TAG, String.format(Locale.US, "Does not meet criteria. API: %d, BackupsEnabled: %s, HasFiles: %s", + Build.VERSION.SDK_INT, + TextSecurePreferences.isBackupEnabled(context), + BackupUtil.hasBackupFiles(context))); + } + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return e instanceof IOException; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull BackupNotificationMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new BackupNotificationMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java index 6e1042fd6..0cc49ab20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java @@ -18,7 +18,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.text.HtmlCompat; -import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; import org.greenrobot.eventbus.EventBus; @@ -165,7 +164,7 @@ public class BackupsPreferenceFragment extends Fragment { } else if (StorageUtil.canWriteInSignalStorageDir()) { try { folder.setVisibility(View.VISIBLE); - folderName.setText(StorageUtil.getBackupDirectory().getPath()); + folderName.setText(StorageUtil.getOrCreateBackupDirectory().getPath()); } catch (NoExternalStorageException e) { Log.w(TAG, "Could not display folder name.", e); } 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 5863ad6b3..e9479adc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.java @@ -15,7 +15,6 @@ import androidx.documentfile.provider.DocumentFile; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.backup.BackupPassphrase; import org.thoughtcrime.securesms.database.NoExternalStorageException; -import org.thoughtcrime.securesms.database.documents.Document; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; @@ -181,7 +180,7 @@ public class BackupUtil { } private static List getAllBackupsNewestFirstLegacy() throws NoExternalStorageException { - File backupDirectory = StorageUtil.getBackupDirectory(); + File backupDirectory = StorageUtil.getOrCreateBackupDirectory(); File[] files = backupDirectory.listFiles(); List backups = new ArrayList<>(files.length); @@ -213,6 +212,20 @@ public class BackupUtil { return result; } + public static boolean hasBackupFiles(@NonNull Context context) { + if (Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) { + try { + File directory = StorageUtil.getBackupDirectory(); + return directory.exists() && directory.isDirectory() && directory.listFiles().length > 0; + } catch (NoExternalStorageException e) { + Log.w(TAG, "Failed to read storage!", e); + return false; + } + } else { + return false; + } + } + private static long getBackupTimestamp(@NonNull String backupName) { String[] prefixSuffix = backupName.split("[.]"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java index 07297bf22..c8347f1d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.permissions.Permissions; +import org.whispersystems.libsignal.util.guava.Optional; import java.io.File; import java.util.List; @@ -28,20 +29,14 @@ public class StorageUtil { private static final String PRODUCTION_PACKAGE_ID = "org.thoughtcrime.securesms"; - public static File getBackupDirectory() throws NoExternalStorageException { + public static File getOrCreateBackupDirectory() throws NoExternalStorageException { File storage = Environment.getExternalStorageDirectory(); if (!storage.canWrite()) { throw new NoExternalStorageException(); } - File signal = new File(storage, "Signal"); - File backups = new File(signal, "Backups"); - - //noinspection ConstantConditions - if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) { - backups = new File(backups, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1)); - } + File backups = getBackupDirectory(); if (!backups.exists()) { if (!backups.mkdirs()) { @@ -52,6 +47,19 @@ public class StorageUtil { return backups; } + public static File getBackupDirectory() throws NoExternalStorageException { + File storage = Environment.getExternalStorageDirectory(); + File signal = new File(storage, "Signal"); + File backups = new File(signal, "Backups"); + + //noinspection ConstantConditions + if (BuildConfig.APPLICATION_ID.startsWith(PRODUCTION_PACKAGE_ID + ".")) { + backups = new File(backups, BuildConfig.APPLICATION_ID.substring(PRODUCTION_PACKAGE_ID.length() + 1)); + } + + return backups; + } + @RequiresApi(24) public static @NonNull String getDisplayPath(@NonNull Context context, @NonNull Uri uri) { String lastPathSegment = Objects.requireNonNull(uri.getLastPathSegment()); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2aec87a2d..c8bd6c368 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2625,12 +2625,11 @@ Last backup: %s In progress Creating backup… - Backups disabled. - Backup failed. + Backup failed Your backup directory has been deleted or moved. Your backup file is too large to store on this volume. There is not enough space to store your backup. - Backup failed for an unknown reason. + Tap to manage backups. %d messages so far Please enter the verification code sent to %s. Wrong number