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 estimatedTotalCount;
BackupEvent(Type type, long count, long estimatedTotalCount) {
public BackupEvent(Type type, long count, long estimatedTotalCount) {
this.type = type;
this.count = count;
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),
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),
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);
private static final short BACKUP_FAILED_ID = 31321;
@ -44,6 +45,7 @@ public enum BackupFileIOError {
.setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(messageId)))
.setContentIntent(pendingIntent)
.build();
@ -51,22 +53,27 @@ public enum BackupFileIOError {
.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);
if (error != null) {
error.postNotification(context);
}
if (error == null && runAttempt > 0) {
if (error == null) {
UNKNOWN.postNotification(context);
}
}
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
if (e.getMessage() != null) {
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE;
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE;
if (e instanceof FullBackupExporter.InvalidBackupStreamException) {
return ATTACHMENT_TOO_LARGE;
} 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;

View file

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

View file

@ -138,11 +138,13 @@ public final class LocalBackupJobApi29 extends BaseJob {
BackupFileIOError.VERIFICATION_FAILED.postNotification(context);
}
} catch (FullBackupExporter.BackupCanceledException e) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, 0, 0));
Log.w(TAG, "Backup cancelled");
throw e;
} catch (IOException 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;
} finally {
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>
<!-- 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>
<!-- 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="ProgressPreference_d_messages_so_far">%d messages so far</string>
<string name="RegistrationActivity_wrong_number">Wrong number</string>