Improve backup creation exception messaging to user.

This commit is contained in:
Cody Henthorne 2022-08-10 11:17:41 -04:00 committed by Alex Hart
parent 36f1183d6c
commit 019025ab8a
6 changed files with 117 additions and 83 deletions

View file

@ -11,7 +11,7 @@ public class BackupEvent {
private final long count; private final long count;
private final long estimatedTotalCount; private final long estimatedTotalCount;
BackupEvent(Type type, long count, long estimatedTotalCount) { public BackupEvent(Type type, long count, long estimatedTotalCount) {
this.type = type; this.type = type;
this.count = count; this.count = count;
this.estimatedTotalCount = estimatedTotalCount; this.estimatedTotalCount = estimatedTotalCount;

View file

@ -22,6 +22,7 @@ public enum BackupFileIOError {
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large), 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), NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
VERIFICATION_FAILED(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_could_not_be_verified), VERIFICATION_FAILED(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_could_not_be_verified),
ATTACHMENT_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_contains_a_very_large_file),
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups); UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
private static final short BACKUP_FAILED_ID = 31321; private static final short BACKUP_FAILED_ID = 31321;
@ -44,6 +45,7 @@ public enum BackupFileIOError {
.setSmallIcon(R.drawable.ic_signal_backup) .setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId)) .setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId)) .setContentText(context.getString(messageId))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(messageId)))
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.build(); .build();
@ -51,22 +53,27 @@ public enum BackupFileIOError {
.notify(BACKUP_FAILED_ID, backupFailedNotification); .notify(BACKUP_FAILED_ID, backupFailedNotification);
} }
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) { public static void postNotificationForException(@NonNull Context context, @NonNull IOException e) {
BackupFileIOError error = getFromException(e); BackupFileIOError error = getFromException(e);
if (error != null) { if (error != null) {
error.postNotification(context); error.postNotification(context);
} }
if (error == null && runAttempt > 0) { if (error == null) {
UNKNOWN.postNotification(context); UNKNOWN.postNotification(context);
} }
} }
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) { private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
if (e.getMessage() != null) { if (e instanceof FullBackupExporter.InvalidBackupStreamException) {
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE; return ATTACHMENT_TOO_LARGE;
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE; } else if (e.getMessage() != null) {
if (e.getMessage().contains("EFBIG")) {
return FILE_TOO_LARGE;
} else if (e.getMessage().contains("ENOSPC")) {
return NOT_ENOUGH_SPACE;
}
} }
return null; return null;

View file

@ -21,7 +21,7 @@ import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil; import org.signal.core.util.SetUtil;
import org.signal.core.util.Stopwatch; import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3; import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil; import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
@ -144,7 +144,7 @@ public class FullBackupExporter extends FullBackupBase {
{ {
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase); BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
int count = 0; int count = 0;
long estimatedCountOutside = 0L; long estimatedCountOutside;
try { try {
outputStream.writeDatabaseVersion(input.getVersion()); outputStream.writeDatabaseVersion(input.getVersion());
@ -351,80 +351,86 @@ public class FullBackupExporter extends FullBackupBase {
return count; return count;
} }
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret,
try { @NonNull Cursor cursor,
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)); @NonNull BackupFrameOutputStream outputStream,
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)); int count,
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE)); long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA)); String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM)); byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
if (!TextUtils.isEmpty(data)) { if (!TextUtils.isEmpty(data)) {
long fileLength = new File(data).length(); long fileLength = new File(data).length();
long dbLength = size; long dbLength = size;
if (size <= 0 || fileLength != dbLength) { if (size <= 0 || fileLength != dbLength) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data); size = calculateVeryOldStreamLength(attachmentSecret, random, data);
Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId)); Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId));
}
} }
}
if (!TextUtils.isEmpty(data) && size > 0) { if (!TextUtils.isEmpty(data) && size > 0) {
InputStream inputStream; try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
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, estimatedCount)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
inputStream.close();
} }
} catch (IOException e) {
Log.w(TAG, e);
} }
return count; return count;
} }
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) { private static int exportSticker(@NonNull AttachmentSecret attachmentSecret,
try { @NonNull Cursor cursor,
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID)); @NonNull BackupFrameOutputStream outputStream,
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH)); int count,
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH)); String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) { if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) { try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size); outputStream.writeSticker(rowId, inputStream, size);
}
} }
} catch (IOException e) {
Log.w(TAG, e);
} }
return count; return count;
} }
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException { private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
long result = 0; long result = 0;
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); int read;
byte[] buffer = new byte[8192];
int read; while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
byte[] buffer = new byte[8192]; result += read;
}
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
result += read;
} }
return result; return result;
} }
private static InputStream openAttachmentStream(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
if (random != null && random.length == 32) {
return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
} else {
return ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
}
}
private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream, private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream,
@NonNull List<String> keysToIncludeInBackup, @NonNull List<String> keysToIncludeInBackup,
int count, int count,
@ -528,20 +534,18 @@ public class FullBackupExporter extends FullBackupBase {
private final Mac mac; private final Mac mac;
private final byte[] cipherKey; private final byte[] cipherKey;
private final byte[] macKey; private final byte[] iv;
private int counter;
private byte[] iv;
private int counter;
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException { private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
try { try {
byte[] salt = Util.getSecretBytes(32); byte[] salt = Util.getSecretBytes(32);
byte[] key = getBackupKey(passphrase, salt); byte[] key = getBackupKey(passphrase, salt);
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64); byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32); byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0]; this.cipherKey = split[0];
this.macKey = split[1]; byte[] macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding"); this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256"); this.mac = Mac.getInstance("HmacSHA256");
@ -576,12 +580,17 @@ public class FullBackupExporter extends FullBackupBase {
} }
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException { public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder() try {
.setAvatar(BackupProtos.Avatar.newBuilder() write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setRecipientId(avatarName) .setAvatar(BackupProtos.Avatar.newBuilder()
.setLength(Util.toIntExact(size)) .setRecipientId(avatarName)
.build()) .setLength(Util.toIntExact(size))
.build()); .build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write avatar to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) { if (writeStream(in) != size) {
throw new IOException("Size mismatch!"); throw new IOException("Size mismatch!");
@ -589,13 +598,18 @@ public class FullBackupExporter extends FullBackupBase {
} }
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException { public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder() try {
.setAttachment(BackupProtos.Attachment.newBuilder() write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setRowId(attachmentId.getRowId()) .setAttachment(BackupProtos.Attachment.newBuilder()
.setAttachmentId(attachmentId.getUniqueId()) .setRowId(attachmentId.getRowId())
.setLength(Util.toIntExact(size)) .setAttachmentId(attachmentId.getUniqueId())
.build()) .setLength(Util.toIntExact(size))
.build()); .build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write " + attachmentId + " to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) { if (writeStream(in) != size) {
throw new IOException("Size mismatch!"); throw new IOException("Size mismatch!");
@ -603,12 +617,17 @@ public class FullBackupExporter extends FullBackupBase {
} }
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException { public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder() try {
.setSticker(BackupProtos.Sticker.newBuilder() write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setRowId(rowId) .setSticker(BackupProtos.Sticker.newBuilder()
.setLength(Util.toIntExact(size)) .setRowId(rowId)
.build()) .setLength(Util.toIntExact(size))
.build()); .build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write sticker to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) { if (writeStream(in) != size) {
throw new IOException("Size mismatch!"); throw new IOException("Size mismatch!");
@ -687,13 +706,14 @@ public class FullBackupExporter extends FullBackupBase {
} }
public interface PostProcessor { public interface PostProcessor {
int postProcess(@NonNull Cursor cursor, int count); int postProcess(@NonNull Cursor cursor, int count) throws IOException;
} }
public interface BackupCancellationSignal { public interface BackupCancellationSignal {
boolean isCanceled(); boolean isCanceled();
} }
public static final class BackupCanceledException extends IOException { public static final class BackupCanceledException extends IOException {}
}
public static final class InvalidBackupStreamException extends IOException {}
} }

View file

@ -145,10 +145,13 @@ public final class LocalBackupJob extends BaseJob {
BackupFileIOError.VERIFICATION_FAILED.postNotification(context); BackupFileIOError.VERIFICATION_FAILED.postNotification(context);
} }
} catch (FullBackupExporter.BackupCanceledException e) { } catch (FullBackupExporter.BackupCanceledException e) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0));
Log.w(TAG, "Backup cancelled"); Log.w(TAG, "Backup cancelled");
throw e; throw e;
} catch (IOException e) { } catch (IOException e) {
BackupFileIOError.postNotificationForException(context, e, getRunAttempt()); Log.w(TAG, "Error during backup!", e);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0));
BackupFileIOError.postNotificationForException(context, e);
throw e; throw e;
} finally { } finally {
if (tempFile.exists()) { if (tempFile.exists()) {

View file

@ -138,11 +138,13 @@ public final class LocalBackupJobApi29 extends BaseJob {
BackupFileIOError.VERIFICATION_FAILED.postNotification(context); BackupFileIOError.VERIFICATION_FAILED.postNotification(context);
} }
} catch (FullBackupExporter.BackupCanceledException e) { } catch (FullBackupExporter.BackupCanceledException e) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0));
Log.w(TAG, "Backup cancelled"); Log.w(TAG, "Backup cancelled");
throw e; throw e;
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Error during backup!", e); Log.w(TAG, "Error during backup!", e);
BackupFileIOError.postNotificationForException(context, e, getRunAttempt()); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0));
BackupFileIOError.postNotificationForException(context, e);
throw e; throw e;
} finally { } finally {
DocumentFile fileToCleanUp = backupDirectory.findFile(temporaryName); DocumentFile fileToCleanUp = backupDirectory.findFile(temporaryName);

View file

@ -3286,6 +3286,8 @@
<string name="LocalBackupJobApi29_there_is_not_enough_space">There is not enough space to store your backup.</string> <string name="LocalBackupJobApi29_there_is_not_enough_space">There is not enough space to store your backup.</string>
<!-- Error message shown if a newly created backup could not be verified as accurate --> <!-- Error message shown if a newly created backup could not be verified as accurate -->
<string name="LocalBackupJobApi29_your_backup_could_not_be_verified">Your recent backup could not be created and verified. Please create a new one.</string> <string name="LocalBackupJobApi29_your_backup_could_not_be_verified">Your recent backup could not be created and verified. Please create a new one.</string>
<!-- Error message shown if a very large attachment is encountered during the backup creation and causes the backup to fail -->
<string name="LocalBackupJobApi29_your_backup_contains_a_very_large_file">Your backup contains a very large file that cannot be backed up. Please delete it and create a new backup.</string>
<string name="LocalBackupJobApi29_tap_to_manage_backups">Tap to manage backups.</string> <string name="LocalBackupJobApi29_tap_to_manage_backups">Tap to manage backups.</string>
<string name="ProgressPreference_d_messages_so_far">%d messages so far</string> <string name="ProgressPreference_d_messages_so_far">%d messages so far</string>
<string name="RegistrationActivity_wrong_number">Wrong number</string> <string name="RegistrationActivity_wrong_number">Wrong number</string>