Improve backup error handling and frequency

This patch improves our backup functionality in a number of ways:
- Only backup the vault when important changes are made, not when the order of
  entries is changed, for instance.
- Don't bubble up backup errors when saving the vault.
- Instead, show an error bar in the main view if the most recent backup attempt
  failed.

<img src="https://alexbakker.me/u/kbhhj2hcgx.png" width="300" />

Clicking on the error bar will take the user to the backup settings.
This commit is contained in:
Alexander Bakker 2020-06-13 12:45:02 +02:00
parent ae5502b650
commit 08ab8237e7
11 changed files with 116 additions and 28 deletions

View file

@ -160,6 +160,14 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply(); _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 String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
}
public boolean isTimeSyncWarningEnabled() { public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true); return _prefs.getBoolean("pref_warn_time_sync", true);
} }

View file

@ -122,9 +122,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics()); this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
} }
protected boolean saveVault() { protected boolean saveVault(boolean backup) {
try { try {
getApp().getVaultManager().save(); getApp().getVaultManager().save(backup);
return true; return true;
} catch (VaultManagerException e) { } catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show(); Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();

View file

@ -36,7 +36,6 @@ import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile; import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials; import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException; import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot; import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -273,10 +272,9 @@ public class AuthActivity extends AegisActivity {
} else { } else {
try { try {
AegisApplication app = getApp(); AegisApplication app = getApp();
VaultManager vault = app.initVaultManager(app.loadVaultFile(), creds); app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) { if (isSlotRepaired) {
vault.setCredentials(creds); saveVault(true);
saveVault();
} }
} catch (VaultManagerException e) { } catch (VaultManagerException e) {
e.printStackTrace(); e.printStackTrace();

View file

@ -467,7 +467,7 @@ public class EditEntryActivity extends AegisActivity {
intent.putExtra("entryUUID", entry.getUUID()); intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete); intent.putExtra("delete", delete);
if (saveVault()) { if (saveVault(true)) {
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
finish(); finish();
} }

View file

@ -16,6 +16,8 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.SubMenu; import android.view.SubMenu;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ActionMode;
@ -81,6 +83,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private SearchView _searchView; private SearchView _searchView;
private FloatingActionsMenu _fabMenu; private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView; private EntryListView _entryListView;
private LinearLayout _btnBackupError;
private FabScrollHelper _fabScrollHelper; private FabScrollHelper _fabScrollHelper;
@ -125,6 +128,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startScanActivity(); startScanActivity();
}); });
_btnBackupError = findViewById(R.id.btn_backup_error);
_btnBackupError.setOnClickListener(view -> {
startPreferencesActivity("pref_backups");
});
_fabScrollHelper = new FabScrollHelper(_fabMenu); _fabScrollHelper = new FabScrollHelper(_fabMenu);
_selectedEntries = new ArrayList<>(); _selectedEntries = new ArrayList<>();
} }
@ -264,7 +272,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} }
} }
saveVault(); saveVault(true);
} }
} }
@ -400,6 +408,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE); startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
} }
private void startPreferencesActivity(String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
}
private void doShortcutActions() { private void doShortcutActions() {
Intent intent = getIntent(); Intent intent = getIntent();
String action = intent.getStringExtra("action"); String action = intent.getStringExtra("action");
@ -489,6 +503,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
handleDeeplink(); handleDeeplink();
updateLockIcon(); updateLockIcon();
doShortcutActions(); doShortcutActions();
updateBackupErrorBar();
} }
@Override @Override
@ -517,7 +532,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.removeEntry(oldEntry); _entryListView.removeEntry(oldEntry);
} }
saveVault(); saveVault(true);
} }
@Override @Override
@ -567,8 +582,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_settings: { case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class); startPreferencesActivity(null);
startActivityForResult(intent, CODE_PREFERENCES);
return true; return true;
} }
case R.id.action_about: { case R.id.action_about: {
@ -647,6 +661,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} }
} }
private void updateBackupErrorBar() {
String error = null;
if (_app.getPreferences().isBackupsEnabled()) {
error = _app.getPreferences().getBackupsError();
}
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
}
@Override @Override
public void onEntryClick(VaultEntry entry) { public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) { if (_actionMode != null) {
@ -694,12 +717,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override @Override
public void onEntryDrop(VaultEntry entry) { public void onEntryDrop(VaultEntry entry) {
saveVault(); saveVault(false);
} }
@Override @Override
public void onEntryChange(VaultEntry entry) { public void onEntryChange(VaultEntry entry) {
saveVault(); saveVault(true);
} }
public void onEntryCopy(VaultEntry entry) { public void onEntryCopy(VaultEntry entry) {

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.ui; package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
@ -24,6 +25,18 @@ public class PreferencesActivity extends AegisActivity {
} }
} }
@Override
public void onResume() {
super.onResume();
Intent intent = getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
_fragment.scrollToPreference(preference);
intent.removeExtra("pref");
}
}
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
// pass permission request results to the fragment // pass permission request results to the fragment

View file

@ -434,8 +434,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
} }
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onResume() {
super.onActivityCreated(savedInstanceState); super.onResume();
updateEncryptionPreferences(); updateEncryptionPreferences();
updateBackupPreference(); updateBackupPreference();
} }
@ -749,13 +749,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_prefs.setBackupsLocation(uri); _prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true); _prefs.setIsBackupsEnabled(true);
_prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString()))); _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference(); updateBackupPreference();
} }
private boolean saveVault() { private boolean saveVault() {
try { try {
_vault.save(); _vault.save(true);
} catch (VaultManagerException e) { } catch (VaultManagerException e) {
e.printStackTrace(); e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e); Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);

View file

@ -96,7 +96,7 @@ public class VaultManager {
} }
} }
public void save() throws VaultManagerException { public void save(boolean backup) throws VaultManagerException {
try { try {
JSONObject obj = _vault.toJson(); JSONObject obj = _vault.toJson();
@ -108,13 +108,18 @@ public class VaultManager {
} }
save(_context, file); save(_context, file);
if (_prefs.isBackupsEnabled()) {
backup();
}
} catch (VaultFileException e) { } catch (VaultFileException e) {
throw new VaultManagerException(e); throw new VaultManagerException(e);
} }
if (backup && _prefs.isBackupsEnabled()) {
try {
backup();
_prefs.setBackupsError(null);
} catch (VaultManagerException e) {
_prefs.setBackupsError(e);
}
}
} }
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException { public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
@ -190,11 +195,11 @@ public class VaultManager {
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException { public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
_creds = creds; _creds = creds;
save(); save(true);
} }
public void disableEncryption() throws VaultManagerException { public void disableEncryption() throws VaultManagerException {
_creds = null; _creds = null;
save(); save(true);
} }
} }

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
</vector>

View file

@ -8,12 +8,43 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.ui.MainActivity"> tools:context="com.beemdevelopment.aegis.ui.MainActivity">
<fragment <LinearLayout
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/> android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/btn_backup_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp"
android:background="@color/colorAccent"
android:foreground="?android:selectableItemBackground"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/icon_primary_inverted"
android:src="@drawable/ic_alert_black_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/backup_error_bar_message"
android:textColor="@color/primary_text_inverted"
android:layout_marginStart="5dp" />
</LinearLayout>
<fragment
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="fill_parent"
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</LinearLayout>
<!-- note: the fab should always be the last element to be sure it's displayed on top --> <!-- note: the fab should always be the last element to be sure it's displayed on top -->
<com.getbase.floatingactionbutton.FloatingActionsMenu <com.getbase.floatingactionbutton.FloatingActionsMenu

View file

@ -248,6 +248,7 @@
<string name="google_qr_export_unrelated">Unrelated QR code found. Try restarting the scanner.</string> <string name="google_qr_export_unrelated">Unrelated QR code found. Try restarting the scanner.</string>
<string name="google_qr_export_scanned">Scanned %d/%d QR codes</string> <string name="google_qr_export_scanned">Scanned %d/%d QR codes</string>
<string name="google_qr_export_unexpected">Expected QR code #%d, but scanned #%d instead</string> <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="custom_notices_format_style" translatable="false" > <string name="custom_notices_format_style" translatable="false" >
body { body {