diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt new file mode 100644 index 000000000..aab1210ed --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.backup + +import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.GroupReceiptDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.SmsDatabase + +/** + * Queries used by backup exporter to estimate total counts for various complicated tables. + */ +object BackupCountQueries { + + const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.VIEW_ONCE} <= 0" + + const val smsCount: String = "SELECT COUNT(*) FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.EXPIRES_IN} <= 0" + + @get:JvmStatic + val groupReceiptCount: String = """ + SELECT COUNT(*) FROM ${GroupReceiptDatabase.TABLE_NAME} + INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${GroupReceiptDatabase.TABLE_NAME}.${GroupReceiptDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID} + WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0 + """.trimIndent() + + @get:JvmStatic + val attachmentCount: String = """ + SELECT COUNT(*) FROM ${AttachmentDatabase.TABLE_NAME} + INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${AttachmentDatabase.TABLE_NAME}.${AttachmentDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID} + WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0 + """.trimIndent() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupBase.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupBase.java index d6e28422f..54585bd8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupBase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupBase.java @@ -13,13 +13,12 @@ import java.security.NoSuchAlgorithmException; public abstract class FullBackupBase { - @SuppressWarnings("unused") - private static final String TAG = Log.tag(FullBackupBase.class); + private static final int DIGEST_ROUNDS = 250_000; static class BackupStream { static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) { try { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0)); MessageDigest digest = MessageDigest.getInstance("SHA-512"); byte[] input = passphrase.replace(" ", "").getBytes(); @@ -27,8 +26,8 @@ public abstract class FullBackupBase { if (salt != null) digest.update(salt); - for (int i=0;i<250000;i++) { - if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); + for (int i = 0; i < DIGEST_ROUNDS; i++) { + if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0)); digest.update(hash); hash = digest.digest(input); } @@ -47,20 +46,34 @@ public abstract class FullBackupBase { } private final Type type; - private final int count; + private final long count; + private final long estimatedTotalCount; - BackupEvent(Type type, int count) { - this.type = type; - this.count = count; + BackupEvent(Type type, long count, long estimatedTotalCount) { + this.type = type; + this.count = count; + this.estimatedTotalCount = estimatedTotalCount; } public Type getType() { return type; } - public int getCount() { + public long getCount() { return count; } + + public long getEstimatedTotalCount() { + return estimatedTotalCount; + } + + public double getCompletionPercentage() { + if (estimatedTotalCount == 0) { + return 0; + } + + return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 586c1390d..9b1038470 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -75,6 +75,11 @@ public class FullBackupExporter extends FullBackupBase { private static final String TAG = Log.tag(FullBackupExporter.class); + private static final long DATABASE_VERSION_RECORD_COUNT = 1L; + private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L; + private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L; + private static final long FINAL_MESSAGE_COUNT = 1L; + private static final Set BLACKLISTED_TABLES = SetUtil.newHashSet( SignedPreKeyDatabase.TABLE_NAME, OneTimePreKeyDatabase.TABLE_NAME, @@ -134,58 +139,62 @@ public class FullBackupExporter extends FullBackupBase { @NonNull BackupCancellationSignal cancellationSignal) throws IOException { - BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase); - int count = 0; + BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase); + int count = 0; + long estimatedCountOutside = 0L; try { outputStream.writeDatabaseVersion(input.getVersion()); count++; List tables = exportSchema(input, outputStream); - count += tables.size() * 3; + count += tables.size() * TABLE_RECORD_COUNT_MULTIPLIER; + + final long estimatedCount = calculateCount(context, input, tables); + estimatedCountOutside = estimatedCount; Stopwatch stopwatch = new Stopwatch("Backup"); for (String table : tables) { throwIfCanceled(cancellationSignal); if (table.equals(MmsDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, cancellationSignal); + count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal); } else if (table.equals(SmsDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, cancellationSignal); + count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal); } else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, cancellationSignal); + count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal); } else if (table.equals(AttachmentDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal); + count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal); } else if (table.equals(StickerDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal); + count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal); } else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) { - count = exportTable(table, input, outputStream, null, null, count, cancellationSignal); + count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal); } stopwatch.split("table::" + table); } for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { throwIfCanceled(cancellationSignal); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(preference); } - + for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) { throwIfCanceled(cancellationSignal); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(preference); } stopwatch.split("prefs"); - count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, cancellationSignal); + count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, estimatedCount, cancellationSignal); stopwatch.split("key_values"); for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) { throwIfCanceled(cancellationSignal); if (avatar != null) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength()); } } @@ -198,7 +207,49 @@ public class FullBackupExporter extends FullBackupBase { if (closeOutputStream) { outputStream.close(); } - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside)); + } + } + + private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List tables) { + long count = DATABASE_VERSION_RECORD_COUNT + TABLE_RECORD_COUNT_MULTIPLIER * tables.size(); + + for (String table : tables) { + if (table.equals(MmsDatabase.TABLE_NAME)) { + count += getCount(input, BackupCountQueries.mmsCount); + } else if (table.equals(SmsDatabase.TABLE_NAME)) { + count += getCount(input, BackupCountQueries.smsCount); + } else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) { + count += getCount(input, BackupCountQueries.getGroupReceiptCount()); + } else if (table.equals(AttachmentDatabase.TABLE_NAME)) { + count += getCount(input, BackupCountQueries.getAttachmentCount()); + } else if (table.equals(StickerDatabase.TABLE_NAME)) { + count += getCount(input, "SELECT COUNT(*) FROM " + table); + } else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) { + count += getCount(input, "SELECT COUNT(*) FROM " + table); + } + } + + count += IDENTITY_KEY_BACKUP_RECORD_COUNT; + + count += TextSecurePreferences.getPreferencesToSaveToBackupCount(context); + + KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()) + .getDataSet(); + for (String key : SignalStore.getKeysToIncludeInBackup()) { + if (dataSet.containsKey(key)) { + count++; + } + } + + count += AvatarHelper.getAvatarCount(context); + + return count + FINAL_MESSAGE_COUNT; + } + + private static long getCount(@NonNull SQLiteDatabase input, @NonNull String query) { + try (Cursor cursor = input.rawQuery(query)) { + return cursor.moveToFirst() ? cursor.getLong(0) : 0; } } @@ -244,6 +295,7 @@ public class FullBackupExporter extends FullBackupBase { @Nullable Predicate predicate, @Nullable PostProcessor postProcess, int count, + long estimatedCount, @NonNull BackupCancellationSignal cancellationSignal) throws IOException { @@ -283,7 +335,7 @@ public class FullBackupExporter extends FullBackupBase { statement.append(')'); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(statementBuilder.setStatement(statement.toString()).build()); if (postProcess != null) { @@ -296,7 +348,7 @@ public class FullBackupExporter extends FullBackupBase { return count; } - private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) { + private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { try { long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)); long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)); @@ -321,7 +373,7 @@ public class FullBackupExporter extends FullBackupBase { if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); } } catch (IOException e) { @@ -331,7 +383,7 @@ public class FullBackupExporter extends FullBackupBase { return count; } - private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) { + private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { try { long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID)); long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH)); @@ -340,7 +392,7 @@ public class FullBackupExporter extends FullBackupBase { byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); if (!TextUtils.isEmpty(data) && size > 0) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); outputStream.writeSticker(rowId, inputStream, size); } @@ -371,6 +423,7 @@ public class FullBackupExporter extends FullBackupBase { private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream, @NonNull List keysToIncludeInBackup, int count, + long estimatedCount, BackupCancellationSignal cancellationSignal) throws IOException { KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()) @@ -401,7 +454,7 @@ public class FullBackupExporter extends FullBackupBase { throw new AssertionError("Unknown type: " + type); } - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); outputStream.write(builder.build()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index c07cb1a9e..2b7bae4e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -95,7 +95,7 @@ public class FullBackupImporter extends FullBackupBase { BackupFrame frame; while (!(frame = inputStream.readFrame()).getEnd()) { - if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count)); + if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count, 0)); count++; if (frame.hasVersion()) processVersion(db, frame.getVersion()); @@ -115,7 +115,7 @@ public class FullBackupImporter extends FullBackupBase { keyValueDatabase.endTransaction(); } - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count)); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count, 0)); } private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{ diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java index 4e65f8871..305bfd8ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java @@ -59,7 +59,7 @@ final class OldDeviceClientTask implements ClientTask { public void onEvent(FullBackupBase.BackupEvent event) { if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) { if (System.currentTimeMillis() > lastProgressUpdate + PROGRESS_UPDATE_THROTTLE) { - EventBus.getDefault().post(new Status(event.getCount(), false)); + EventBus.getDefault().post(new Status(event.getCount(), event.getEstimatedTotalCount(), event.getCompletionPercentage(), false)); lastProgressUpdate = System.currentTimeMillis(); } } @@ -68,22 +68,34 @@ final class OldDeviceClientTask implements ClientTask { @Override public void success() { SignalStore.misc().markOldDeviceTransferLocked(); - EventBus.getDefault().post(new Status(0, true)); + EventBus.getDefault().post(new Status(0, 0, 0,true)); } public static final class Status { private final long messages; + private final long estimatedMessages; + private final double completionPercentage; private final boolean done; - public Status(long messages, boolean done) { - this.messages = messages; - this.done = done; + public Status(long messages, long estimatedMessages, double completionPercentage, boolean done) { + this.messages = messages; + this.estimatedMessages = estimatedMessages; + this.completionPercentage = completionPercentage; + this.done = done; } public long getMessageCount() { return messages; } + public long getEstimatedMessageCount() { + return estimatedMessages; + } + + public double getCompletionPercentage() { + return completionPercentage; + } + public boolean isDone() { return done; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceTransferFragment.java index 15e85a49e..ba32523d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceTransferFragment.java @@ -15,6 +15,9 @@ import org.signal.devicetransfer.TransferStatus; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.devicetransfer.DeviceTransferFragment; +import java.text.NumberFormat; +import java.util.Locale; + /** * Shows transfer progress on the old device. Most logic is in {@link DeviceTransferFragment} * and it delegates to this class for strings, navigation, and updating progress. @@ -52,6 +55,14 @@ public final class OldDeviceTransferFragment extends DeviceTransferFragment { } private class ClientTaskListener { + private final NumberFormat formatter; + + public ClientTaskListener() { + formatter = NumberFormat.getInstance(); + formatter.setMinimumFractionDigits(1); + formatter.setMaximumFractionDigits(1); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(@NonNull OldDeviceClientTask.Status event) { if (event.isDone()) { @@ -61,7 +72,11 @@ public final class OldDeviceTransferFragment extends DeviceTransferFragment { DeviceToDeviceTransferService.stop(requireContext()); NavHostFragment.findNavController(OldDeviceTransferFragment.this).navigate(R.id.action_oldDeviceTransfer_to_oldDeviceTransferComplete); } else { - status.setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount())); + if (event.getEstimatedMessageCount() == 0) { + status.setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount())); + } else { + status.setText(getString(R.string.DeviceTransfer__s_of_messages_so_far, formatter.format(event.getCompletionPercentage()))); + } } } } 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 e78a2a8ab..8eaf7390d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -5,10 +5,14 @@ import android.Manifest; import androidx.annotation.NonNull; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.backup.BackupFileIOError; import org.thoughtcrime.securesms.backup.BackupPassphrase; +import org.thoughtcrime.securesms.backup.FullBackupBase; import org.thoughtcrime.securesms.backup.FullBackupExporter; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.database.NoExternalStorageException; @@ -85,11 +89,14 @@ public final class LocalBackupJob extends BaseJob { throw new IOException("No external storage permission!"); } + ProgressUpdater updater = new ProgressUpdater(); try (NotificationController notification = GenericForegroundService.startForegroundTask(context, - context.getString(R.string.LocalBackupJob_creating_backup), + context.getString(R.string.LocalBackupJob_creating_signal_backup), NotificationChannels.BACKUPS, R.drawable.ic_signal_backup)) { + updater.setNotification(notification); + EventBus.getDefault().register(updater); notification.setIndeterminateProgress(); String backupPassword = BackupPassphrase.get(context); @@ -139,6 +146,9 @@ public final class LocalBackupJob extends BaseJob { } BackupUtil.deleteOldBackups(); + } finally { + EventBus.getDefault().unregister(updater); + updater.setNotification(null); } } @@ -166,6 +176,29 @@ public final class LocalBackupJob extends BaseJob { public void onFailure() { } + private static class ProgressUpdater { + private NotificationController notification; + + @Subscribe(threadMode = ThreadMode.POSTING) + public void onEvent(FullBackupBase.BackupEvent event) { + if (notification == null) { + return; + } + + if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) { + if (event.getEstimatedTotalCount() == 0) { + notification.setIndeterminateProgress(); + } else { + notification.setProgress(100, (int) event.getCompletionPercentage()); + } + } + } + + public void setNotification(NotificationController notification) { + this.notification = notification; + } + } + public static class Factory implements Job.Factory { @Override public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) { 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 efa59255d..5b041e6bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupJobApi29.java @@ -6,10 +6,14 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.backup.BackupFileIOError; import org.thoughtcrime.securesms.backup.BackupPassphrase; +import org.thoughtcrime.securesms.backup.FullBackupBase; import org.thoughtcrime.securesms.backup.FullBackupExporter; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -70,11 +74,14 @@ public final class LocalBackupJobApi29 extends BaseJob { throw new IOException("Backup Directory has not been selected!"); } + ProgressUpdater updater = new ProgressUpdater(); try (NotificationController notification = GenericForegroundService.startForegroundTask(context, - context.getString(R.string.LocalBackupJob_creating_backup), + context.getString(R.string.LocalBackupJob_creating_signal_backup), NotificationChannels.BACKUPS, R.drawable.ic_signal_backup)) { + updater.setNotification(notification); + EventBus.getDefault().register(updater); notification.setIndeterminateProgress(); String backupPassword = BackupPassphrase.get(context); @@ -135,6 +142,9 @@ public final class LocalBackupJobApi29 extends BaseJob { } BackupUtil.deleteOldBackups(); + } finally { + EventBus.getDefault().unregister(updater); + updater.setNotification(null); } } @@ -162,6 +172,29 @@ public final class LocalBackupJobApi29 extends BaseJob { public void onFailure() { } + private static class ProgressUpdater { + private NotificationController notification; + + @Subscribe(threadMode = ThreadMode.POSTING) + public void onEvent(FullBackupBase.BackupEvent event) { + if (notification == null) { + return; + } + + if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) { + if (event.getEstimatedTotalCount() == 0) { + notification.setIndeterminateProgress(); + } else { + notification.setProgress(100, (int) event.getCompletionPercentage()); + } + } + } + + public void setNotification(NotificationController notification) { + this.notification = notification; + } + } + public static class Factory implements Job.Factory { @Override public @NonNull 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 a8d7640fc..d673a2630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.StorageUtil; +import java.text.NumberFormat; import java.util.Locale; import java.util.Objects; @@ -54,6 +55,8 @@ public class BackupsPreferenceFragment extends Fragment { private ProgressBar progress; private TextView progressSummary; + private final NumberFormat formatter = NumberFormat.getInstance(); + @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_backups, container, false); @@ -75,6 +78,9 @@ public class BackupsPreferenceFragment extends Fragment { create.setOnClickListener(unused -> onCreateClicked()); verify.setOnClickListener(unused -> BackupDialog.showVerifyBackupPassphraseDialog(requireContext())); + formatter.setMinimumFractionDigits(1); + formatter.setMaximumFractionDigits(1); + EventBus.getDefault().register(this); } @@ -120,8 +126,19 @@ public class BackupsPreferenceFragment extends Fragment { create.setEnabled(false); summary.setText(getString(R.string.BackupsPreferenceFragment__in_progress)); progress.setVisibility(View.VISIBLE); - progressSummary.setVisibility(View.VISIBLE); - progressSummary.setText(getString(R.string.BackupsPreferenceFragment__d_so_far, event.getCount())); + progressSummary.setVisibility(event.getCount() > 0 ? View.VISIBLE : View.GONE); + + if (event.getEstimatedTotalCount() == 0) { + progress.setIndeterminate(true); + progressSummary.setText(getString(R.string.BackupsPreferenceFragment__d_so_far, event.getCount())); + } else { + double completionPercentage = event.getCompletionPercentage(); + + progress.setIndeterminate(false); + progress.setMax(100); + progress.setProgress((int) completionPercentage); + progressSummary.setText(getString(R.string.BackupsPreferenceFragment__s_so_far, formatter.format(completionPercentage))); + } } else if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) { create.setEnabled(true); progress.setVisibility(View.GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 104907c2b..3d8dc66e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -34,6 +34,13 @@ public class AvatarHelper { private static final String AVATAR_DIRECTORY = "avatars"; + public static long getAvatarCount(@NonNull Context context) { + File avatarDirectory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE); + String[] results = avatarDirectory.list(); + + return results == null ? 0 : results.length; + } + /** * Retrieves an iterable set of avatars. Only intended to be used during backup. */ 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 2068d734a..9b4ff6e6d 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 @@ -328,7 +328,7 @@ public final class RestoreBackupFragment extends LoggingFragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(@NonNull FullBackupBase.BackupEvent event) { - int count = event.getCount(); + long count = event.getCount(); if (count == 0) { restoreBackupProgress.setText(R.string.RegistrationActivity_checking); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index a60c6c978..48f14e2ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -230,6 +230,31 @@ public class TextSecurePreferences { MEDIA_DOWNLOAD_WIFI_PREF, MEDIA_DOWNLOAD_ROAMING_PREF}; + public static long getPreferencesToSaveToBackupCount(@NonNull Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + long count = 0; + + for (String booleanPreference : booleanPreferencesToBackup) { + if (preferences.contains(booleanPreference)) { + count++; + } + } + + for (String stringPreference : stringPreferencesToBackup) { + if (preferences.contains(stringPreference)) { + count++; + } + } + + for (String stringSetPreference : stringSetPreferencesToBackup) { + if (preferences.contains(stringSetPreference)) { + count++; + } + } + + return count; + } + public static List getPreferencesToSaveToBackup(@NonNull Context context) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); List backupProtos = new ArrayList<>(); diff --git a/app/src/main/res/layout/fragment_backups.xml b/app/src/main/res/layout/fragment_backups.xml index 6cf38307e..ee4d1093b 100644 --- a/app/src/main/res/layout/fragment_backups.xml +++ b/app/src/main/res/layout/fragment_backups.xml @@ -64,7 +64,6 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:indeterminate="true" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/fragment_backup_progress_summary" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b983e0164..6e520e81e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -470,6 +470,8 @@ Learn more In progress… %1$d so far… + + %1$s%% so far… Signal requires external storage permission in order to create backups, but it has been permanently denied. Please continue to app settings, select \"Permissions\" and enable \"Storage\". @@ -3084,7 +3086,7 @@ Verify You successfully entered your backup passphrase Passphrase was not correct - Creating backup… + Creating Signal backup… Backup failed Your backup directory has been deleted or moved. Your backup file is too large to store on this volume. @@ -3213,6 +3215,8 @@ Transferring data Keep both devices near each other. Do not turn off the devices and keep Signal open. Transfers are end-to-end encrypted. %1$d messages so far… + + %1$s%% of messages so far… Cancel Try again Stop transfer? diff --git a/device-transfer/lib/src/main/java/org/signal/devicetransfer/DeviceToDeviceTransferService.java b/device-transfer/lib/src/main/java/org/signal/devicetransfer/DeviceToDeviceTransferService.java index 6c68a2396..cbe2a76bb 100644 --- a/device-transfer/lib/src/main/java/org/signal/devicetransfer/DeviceToDeviceTransferService.java +++ b/device-transfer/lib/src/main/java/org/signal/devicetransfer/DeviceToDeviceTransferService.java @@ -8,10 +8,12 @@ import android.content.Intent; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -21,6 +23,7 @@ import org.signal.core.util.logging.Log; import java.io.Serializable; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * Foreground service to help manage interactions with the {@link DeviceTransferClient} and @@ -44,6 +47,7 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa private PendingIntent pendingIntent; private DeviceTransferServer server; private DeviceTransferClient client; + private PowerManager.WakeLock wakeLock; public static void startServer(@NonNull Context context, @NonNull ServerTask serverTask, @@ -114,6 +118,10 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa server = null; } + if (wakeLock != null) { + wakeLock.release(); + } + super.onDestroy(); } @@ -143,6 +151,7 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa server = new DeviceTransferServer(getApplicationContext(), (ServerTask) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_TASK)), this); + acquireWakeLock(); server.start(); } else { Log.i(TAG, "Can't start server, already started."); @@ -156,6 +165,7 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa client = new DeviceTransferClient(getApplicationContext(), (ClientTask) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_TASK)), this); + acquireWakeLock(); client.start(); } else { Log.i(TAG, "Can't start client, client already started."); @@ -187,6 +197,19 @@ public class DeviceToDeviceTransferService extends Service implements ShutdownCa }); } + private void acquireWakeLock() { + if (wakeLock == null) { + PowerManager powerManager = ContextCompat.getSystemService(this, PowerManager.class); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:d2dpartial"); + } + } + + if (!wakeLock.isHeld()) { + wakeLock.acquire(TimeUnit.HOURS.toMillis(2)); + } + } + private void updateNotification(@NonNull TransferStatus transferStatus) { if (notificationData != null && (client != null || server != null)) { startForeground(notificationData.notificationId, createNotification(transferStatus, notificationData));