diff --git a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java index 4d688637..05cf4047 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java +++ b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java @@ -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() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java index 6d6faa94..73d23418 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/Preferences.java +++ b/app/src/main/java/com/beemdevelopment/aegis/Preferences.java @@ -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); + } + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 81ce3235..eb035b39 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -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()) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java index d44b75ec..4347704f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/dialogs/Dialogs.java @@ -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 messages, DialogInterface.OnClickListener listener) { Dialogs.showSecureDialog(new AlertDialog.Builder(context) diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/BackupsPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/BackupsPreferencesFragment.java index dc410e37..fc052bba 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/BackupsPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/BackupsPreferencesFragment.java @@ -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() { diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java index 4a7ed0e7..5108a70f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java @@ -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)); } }); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java index a28ac4ed..7e56cf79 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java @@ -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() )); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java index c99b5687..61cdc970 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -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)); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b6e57c1..0816bc76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,6 +205,9 @@ An error occurred while disabling encryption The backup was scheduled successfully An error occurred while trying to create a backup + Most recent backup successful: %s + Most recent backup failed: %s + No backups have been made yet 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 "debloat" your device, you may have accidentally deleted it and will have to reinstall it. An error occurred while trying to import an icon pack The icon pack you\'re trying to import already exists. Do you want to overwrite it? @@ -351,6 +354,9 @@ Expected QR code #%d, but scanned #%d instead Vault backup failed recently + 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. + Aegis\' built-in automatic backups + Android\'s cloud backup system Recent vault changes are not backed up The vault was recently exported in plain text Don\'t show this warning again diff --git a/app/src/main/res/xml/preferences_backups.xml b/app/src/main/res/xml/preferences_backups.xml index 3ec41ac3..651bdaa8 100644 --- a/app/src/main/res/xml/preferences_backups.xml +++ b/app/src/main/res/xml/preferences_backups.xml @@ -25,6 +25,11 @@ android:key="pref_backups_versions" android:title="@string/pref_backups_versions_title" app:iconSpaceReserved="false"/> + +