Store and display backup error messages more clearly

This commit is contained in:
Alexander Bakker 2022-10-05 18:21:50 +02:00
parent 4427498d5e
commit 8ae8130b71
10 changed files with 242 additions and 34 deletions

View file

@ -29,7 +29,7 @@ public class AegisBackupAgent extends BackupAgent {
public void onCreate() {
super.onCreate();
// cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this);
}
@ -40,7 +40,7 @@ public class AegisBackupAgent extends BackupAgent {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
if (isD2D) {
Log.i(TAG, "onFullBackup(): allowing D2D transfer");
@ -49,31 +49,39 @@ public class AegisBackupAgent extends BackupAgent {
return;
}
// first copy the vault to the files/backup directory
// We perform a catch of any Exception here to make sure we also
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
fullBackup(data);
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
throw e;
}
Log.i(TAG, "onFullBackup() finished");
}
private void fullBackup(FullBackupDataOutput data) throws IOException {
// First copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
createBackupDir();
VaultFile vaultFile = VaultRepository.readVaultFile(this);
byte[] bytes = vaultFile.exportable().toBytes();
outputStream.write(bytes);
} catch (VaultRepositoryException | IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir();
throw new IOException(e);
}
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
// Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
throw e;
} finally {
deleteBackupDir();
}
Log.i(TAG, "onFullBackup() finished");
}
@Override
@ -114,14 +122,16 @@ public class AegisBackupAgent extends BackupAgent {
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
if (dir == null || (!dir.exists() && !dir.mkdir())) {
throw new IOException(String.format("Unable to create backup directory: %s", dir));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true);
if (dir != null) {
IOUtils.clearDirectory(dir, true);
}
}
private File getVaultBackupFile() {

View file

@ -7,10 +7,15 @@ import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -259,6 +264,7 @@ public class Preferences {
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
setAndroidBackupResult(null);
}
public boolean isBackupsEnabled() {
@ -267,6 +273,7 @@ public class Preferences {
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
setBuiltInBackupResult(null);
}
public Uri getBackupsLocation() {
@ -298,12 +305,64 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
public void setAndroidBackupResult(@Nullable BackupResult res) {
setBackupResult(false, res);
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
public void setBuiltInBackupResult(@Nullable BackupResult res) {
setBackupResult(true, res);
}
@Nullable
public BackupResult getAndroidBackupResult() {
return getBackupResult(false);
}
@Nullable
public BackupResult getBuiltInBackupResult() {
return getBackupResult(true);
}
@Nullable
public Preferences.BackupResult getErroredBackupResult() {
Preferences.BackupResult res = getBuiltInBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
res = getAndroidBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
return null;
}
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
String json = null;
if (res != null) {
res.setIsBuiltIn(isBuiltInBackup);
json = res.toJson();
}
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
}
@Nullable
private BackupResult getBackupResult(boolean isBuiltInBackup) {
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
if (json == null) {
return null;
}
try {
BackupResult res = BackupResult.fromJson(json);
res.setIsBuiltIn(isBuiltInBackup);
return res;
} catch (JSONException e) {
return null;
}
}
private static String getBackupResultKey(boolean isBuiltInBackup) {
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
}
public void setIsBackupReminderNeeded(boolean needed) {
@ -382,4 +441,63 @@ public class Preferences {
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString());
}
private BackupResult(Date time, @Nullable String error) {
_time = time;
_error = error;
}
@Nullable
public String getError() {
return _error;
}
public boolean isSuccessful() {
return _error == null;
}
public Date getTime() {
return _time;
}
public String getHumanReadableTime() {
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(_time);
}
public boolean isBuiltIn() {
return _isBuiltIn;
}
private void setIsBuiltIn(boolean isBuiltIn) {
_isBuiltIn = isBuiltIn;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj.toString();
}
public static BackupResult fromJson(String json) throws JSONException {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
return new BackupResult(new Date(time), error);
}
}
}

View file

@ -41,7 +41,6 @@ import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
@ -781,15 +780,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void updateErrorBar() {
String backupError = null;
if (_prefs.isBackupsEnabled()) {
backupError = _prefs.getBackupsError();
}
if (backupError != null) {
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
if (backupRes != null) {
_textErrorBar.setText(R.string.backup_error_bar_message);
_btnErrorBar.setOnClickListener(view -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
});
_btnErrorBar.setVisibility(View.VISIBLE);
} else if (_prefs.isBackupsReminderNeeded()) {

View file

@ -36,6 +36,7 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -403,6 +404,12 @@ public class Dialogs {
Dialogs.showSecureDialog(dialog);
}
public static void showBackupErrorDialog(Context context, Preferences.BackupResult backupRes, DialogInterface.OnClickListener listener) {
String system = context.getString(backupRes.isBuiltIn() ? R.string.backup_system_builtin : R.string.backup_system_android);
String message = context.getString(R.string.backup_error_dialog_details, system, backupRes.getHumanReadableTime());
Dialogs.showErrorDialog(context, message, backupRes.getError(), listener);
}
public static void showMultiMessageDialog(
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)

View file

@ -2,10 +2,16 @@ package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
@ -21,6 +27,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
private Preference _builtinBackupStatusPreference;
private Preference _androidBackupStatusPreference;
@Override
public void onResume() {
super.onResume();
@ -32,6 +41,23 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_backups);
_builtinBackupStatusPreference = requirePreference("pref_status_backup_builtin");
_builtinBackupStatusPreference.setOnPreferenceClickListener(preference -> {
Preferences.BackupResult backupRes = _prefs.getBuiltInBackupResult();
if (backupRes != null && !backupRes.isSuccessful()) {
Dialogs.showBackupErrorDialog(requireContext(), backupRes, null);
}
return true;
});
_androidBackupStatusPreference = requirePreference("pref_status_backup_android");
_androidBackupStatusPreference.setOnPreferenceClickListener(preference -> {
Preferences.BackupResult backupRes = _prefs.getAndroidBackupResult();
if (backupRes != null && !backupRes.isSuccessful()) {
Dialogs.showBackupErrorDialog(requireContext(), backupRes, null);
}
return true;
});
_backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
@ -48,7 +74,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
updateBackupPreference();
_vaultManager.scheduleAndroidBackup();
if ((boolean) newValue) {
_vaultManager.scheduleAndroidBackup();
}
return false;
});
@ -66,6 +94,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (_prefs.isBackupsEnabled()) {
scheduleBackup();
_builtinBackupStatusPreference.setVisible(false);
}
return true;
});
@ -100,7 +129,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
scheduleBackup();
@ -117,6 +145,38 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
if (backupEnabled) {
Preferences.BackupResult backupRes = _prefs.getBuiltInBackupResult();
_builtinBackupStatusPreference.setSummary(getBackupStatusMessage(backupRes));
_builtinBackupStatusPreference.setSelectable(backupRes != null && !backupRes.isSuccessful());
}
if (androidBackupEnabled) {
Preferences.BackupResult backupRes = _prefs.getAndroidBackupResult();
_androidBackupStatusPreference.setSummary(getBackupStatusMessage(backupRes));
_androidBackupStatusPreference.setSelectable(backupRes != null && !backupRes.isSuccessful());
}
_builtinBackupStatusPreference.setVisible(backupEnabled);
_androidBackupStatusPreference.setVisible(androidBackupEnabled);
}
private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult res) {
String message;
int color = R.color.warning_color;
if (res == null) {
message = getString(R.string.backup_status_none);
} else if (res.isSuccessful()) {
color = R.color.success_color;
message = getString(R.string.backup_status_success, res.getHumanReadableTime());
} else {
message = getString(R.string.backup_status_failed, res.getHumanReadableTime());
}
Spannable spannable = new SpannableString(message);
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(color)), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (color == R.color.warning_color) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spannable;
}
private void selectBackupsLocation() {

View file

@ -52,10 +52,10 @@ public class VaultBackupManager {
_executor.execute(() -> {
try {
createBackup(tempFile, dirUri, versionsToKeep);
_prefs.setBackupsError(null);
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
} catch (VaultRepositoryException e) {
e.printStackTrace();
_prefs.setBackupsError(e);
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(e));
}
});
}

View file

@ -126,7 +126,7 @@ public class VaultFile {
return this;
}
return new VaultFile(getContent(), new VaultFile.Header(
return new VaultFile(_content, new VaultFile.Header(
getHeader().getSlots().exportable(),
getHeader().getParams()
));

View file

@ -196,9 +196,8 @@ public class VaultManager {
backedUp = true;
try {
scheduleBackup();
_prefs.setBackupsError(null);
} catch (VaultRepositoryException e) {
_prefs.setBackupsError(e);
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(e));
}
}

View file

@ -205,6 +205,9 @@
<string name="disable_encryption_error">An error occurred while disabling encryption</string>
<string name="backup_successful">The backup was scheduled successfully</string>
<string name="backup_error">An error occurred while trying to create a backup</string>
<string name="backup_status_success">Most recent backup successful: %s</string>
<string name="backup_status_failed">Most recent backup failed: %s</string>
<string name="backup_status_none">No backups have been made yet</string>
<string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string>
<string name="icon_pack_import_error">An error occurred while trying to import an icon pack</string>
<string name="icon_pack_import_exists_error">The icon pack you\'re trying to import already exists. Do you want to overwrite it?</string>
@ -351,6 +354,9 @@
</plurals>
<string name="google_qr_export_unexpected">Expected QR code #%d, but scanned #%d instead</string>
<string name="backup_error_bar_message"><b>Vault backup failed recently</b></string>
<string name="backup_error_dialog_details">A recent vault backup attempt using %s failed because an error occurred. The backup was attempted at: %s. Please check your backup settings to make sure backups can complete successfully.</string>
<string name="backup_system_builtin">Aegis\' built-in automatic backups</string>
<string name="backup_system_android">Android\'s cloud backup system</string>
<string name="backup_reminder_bar_message"><b>Recent vault changes are not backed up</b></string>
<string name="backup_plaintext_export_warning"><b>The vault was recently exported in plain text</b></string>
<string name="pref_show_plaintext_warning_hint">Don\'t show this warning again</string>

View file

@ -25,6 +25,11 @@
android:key="pref_backups_versions"
android:title="@string/pref_backups_versions_title"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_status_backup_builtin"
android:persistent="false"
android:selectable="false"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_cat_backups_android"
@ -36,6 +41,12 @@
android:summary="@string/pref_android_backups_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_status_backup_android"
android:persistent="false"
android:selectable="false"
app:iconSpaceReserved="false"/>
<Preference
android:persistent="false"
android:selectable="false"
android:summary="@string/pref_android_backups_hint"
app:iconSpaceReserved="false"/>