Improve backup creation exception messaging to user.
This commit is contained in:
parent
36f1183d6c
commit
019025ab8a
6 changed files with 117 additions and 83 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue