Merge pull request #1439 from r3dh3ck/feature/single_backup

Single backup
This commit is contained in:
Alexander Bakker 2024-11-15 13:53:54 +01:00 committed by GitHub
commit b70654152d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 254 additions and 12 deletions

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}

View file

@ -6,7 +6,9 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import androidx.preference.PreferenceManager;
import com.beemdevelopment.aegis.util.JsonUtils;
@ -597,6 +599,19 @@ public class Preferences {
}
}
@NonNull
public BackupsVersioningStrategy getBackupVersioningStrategy() {
Uri uri = getBackupsLocation();
if (uri == null) {
return BackupsVersioningStrategy.UNDEFINED;
}
if (DocumentsContractCompat.isTreeUri(uri)) {
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
} else {
return BackupsVersioningStrategy.SINGLE_BACKUP;
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;

View file

@ -19,6 +19,8 @@ import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
@ -27,6 +29,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@ -577,6 +580,54 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
TextView warningText = view.findViewById(R.id.warning_text);
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
}
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
warningText.setVisibility(visibility);
riskAccept.setVisibility(visibility);
});
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(isChecked);
}
});
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int checkedId = radioGroup.getCheckedRadioButtonId();
if (checkedId == keepXVersionsButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
} else if (checkedId == singleBackupButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
alertDialog.setOnShowListener(dialog -> {
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButtonRef.set(positiveButton);
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
radioGroup.check(keepXVersionsButton.getId());
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
radioGroup.check(singleBackupButton.getId());
}
});
showSecureDialog(alertDialog);
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
@ -605,4 +656,8 @@ public class Dialogs {
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
public interface BackupsVersioningStrategyListener {
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
}
}

View file

@ -17,9 +17,11 @@ import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors;
@ -27,6 +29,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private SwitchPreferenceCompat _backupReminderPreference;
private Preference _versioningStrategyPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@ -75,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
} else {
_prefs.setIsBackupsEnabled(false);
updateBackupPreference();
@ -100,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
updateBackupsVersioningStrategySummary();
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
if (strategy == currentStrategy) {
return;
}
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
return true;
});
_androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
@ -110,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = requirePreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
updateBackupsLocationSummary();
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
return false;
});
@ -158,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
scheduleBackup();
updateBackupsVersioningStrategySummary();
updateBackupsLocationSummary();
}
private void updateBackupPreference() {
@ -174,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled);
_versioningStrategyPreference.setVisible(backupEnabled);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
if (backupEnabled) {
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
}
@ -221,6 +252,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return spannable;
}
private void createBackupFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@ -241,6 +280,30 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
}
}
private void updateBackupsVersioningStrategySummary() {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
}
}
private void updateBackupsLocationSummary() {
Uri backupsLocation = _prefs.getBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
String text;
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
text = getString(R.string.pref_backups_location_summary);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
text = getString(R.string.pref_backup_location_summary);
} else {
return;
}
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
_backupsLocationPreference.setSummary(summary);
}
private void updateBackupsVersionsSummary() {
int count = _prefs.getBackupsVersionCount();
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.vault;
import android.content.ContentResolver;
import android.content.Context;
import android.content.UriPermission;
import android.net.Uri;
@ -9,6 +10,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils;
@ -38,6 +40,7 @@ public class VaultBackupManager {
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
public static final String FILENAME_PREFIX = "aegis-backup";
public static final String FILENAME_SINGLE = String.format("%s.json", FILENAME_PREFIX);
private final Context _context;
private final Preferences _prefs;
@ -51,10 +54,10 @@ public class VaultBackupManager {
_auditLogRepository = auditLogRepository;
}
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
public void scheduleBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep) {
_executor.execute(() -> {
try {
createBackup(tempFile, dirUri, versionsToKeep);
createBackup(tempFile, strategy, uri, versionsToKeep);
_auditLogRepository.addBackupCreatedEvent();
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
@ -64,6 +67,46 @@ public class VaultBackupManager {
});
}
private void createBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException {
if (uri == null) {
throw new VaultRepositoryException("getBackupsLocation returned null");
}
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackup(tempFile, uri);
} else if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
createBackup(tempFile, uri, versionsToKeep);
} else {
throw new VaultRepositoryException("Invalid backups versioning strategy");
}
}
private void createBackup(File tempFile, Uri fileUri)
throws VaultRepositoryException, VaultBackupPermissionException {
Log.i(TAG, String.format("Creating backup at %s", fileUri));
try {
if (!hasPermissionsAt(fileUri)) {
throw new VaultBackupPermissionException("No persisted URI permissions");
}
ContentResolver resolver = _context.getContentResolver();
try (FileInputStream inStream = new FileInputStream(tempFile);
OutputStream outStream = resolver.openOutputStream(fileUri, "wt")
) {
if (outStream == null) {
throw new IOException("openOutputStream returned null");
}
IOUtils.copy(inStream, outStream);
} catch (IOException exception) {
throw new VaultRepositoryException(exception);
}
} catch (VaultRepositoryException | VaultBackupPermissionException exception) {
Log.e(TAG, String.format("Unable to create backup: %s", exception));
throw exception;
} finally {
tempFile.delete();
}
}
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException {
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);

View file

@ -5,12 +5,14 @@ import android.app.backup.BackupManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
@ -172,8 +174,11 @@ public class VaultManager {
try (OutputStream outStream = new FileOutputStream(tempFile)) {
_repo.export(outStream);
}
BackupsVersioningStrategy strategy = _prefs.getBackupVersioningStrategy();
Uri uri = _prefs.getBackupsLocation();
int versionsToKeep = _prefs.getBackupsVersionCount();
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
_backups.scheduleBackup(tempFile, strategy, uri, versionsToKeep);
} catch (IOException e) {
throw new VaultRepositoryException(e);
}

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:paddingTop="20dp"
android:paddingEnd="20dp"
android:orientation="vertical">
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/keep_x_versions_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_keep_x_versions" />
<RadioButton
android:id="@+id/single_backup_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_single_backup" />
</RadioGroup>
<TextView
android:id="@+id/warning_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/pref_backups_versioning_strategy_single_backup_warning"
android:textColor="?attr/colorError" />
<CheckBox
android:id="@+id/risk_accept"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/understand_risk_accept" />
</LinearLayout>

View file

@ -67,8 +67,14 @@
<string name="pref_backups_reminder_summary">Show a reminder to backup your vault in case you haven\'t backed up your latest changes.</string>
<string name="pref_backups_reminder_dialog_title">Disable backup reminder</string>
<string name="pref_backups_reminder_dialog_summary">Disabling this reminder means Aegis won\'t tell you whether or not you have changes that are not backed up yet. This puts you at risk of losing access to your tokens. Are you sure you want to disable the reminder?</string>
<string name="pref_backups_location_title">Directory for backup files</string>
<string name="pref_backups_versioning_strategy_title">Backup strategy</string>
<string name="pref_backups_versioning_strategy_keep_x_versions">Keep a number of versions</string>
<string name="pref_backups_versioning_strategy_single_backup">Single backup</string>
<string name="pref_backups_versioning_strategy_single_backup_warning">The selected backup strategy is not reliable and not recommended. A single backup failure could lead to losing your only backup.</string>
<string name="pref_backups_versioning_strategy_dialog_title">Select a backup strategy</string>
<string name="pref_backups_location_title">Backup location</string>
<string name="pref_backups_location_summary">Backups will be stored at</string>
<string name="pref_backup_location_summary">Backup will be stored at</string>
<string name="pref_backups_trigger_title">Trigger backup</string>
<string name="pref_backups_trigger_summary">Manually trigger a backup</string>
<string name="pref_backups_versions_title">Number of versions to keep</string>

View file

@ -23,6 +23,10 @@
android:title="@string/pref_backups_reminder_title"
android:summary="@string/pref_backups_reminder_summary" />
<Preference
android:key="pref_versioning_strategy"
android:title="@string/pref_backups_versioning_strategy_title" />
<Preference
android:key="pref_backups_location"
android:title="@string/pref_backups_location_title"