Skip to content

Commit

Permalink
Implement single backup
Browse files Browse the repository at this point in the history
  • Loading branch information
r3dh3ck committed Nov 6, 2024
1 parent c8d5be6 commit 568683a
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;

public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}
19 changes: 19 additions & 0 deletions app/src/main/java/com/beemdevelopment/aegis/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
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;
Expand Down Expand Up @@ -597,6 +599,23 @@ public Set<UUID> getGroupFilter() {
}
}

@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 boolean isSingleBackupEnabled() {
return getBackupVersioningStrategy() == BackupsVersioningStrategy.SINGLE_BACKUP;
}

public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
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;

Expand All @@ -27,6 +29,7 @@
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;
Expand Down Expand Up @@ -577,6 +580,54 @@ public static void showPartialGoogleAuthImportWarningDialog(Context context, Lis
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()));
Expand Down Expand Up @@ -605,4 +656,8 @@ public interface PasswordSlotListener {
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}

public interface BackupsVersioningStrategyListener {
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
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;

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;
Expand Down Expand Up @@ -100,6 +103,25 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
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();
}
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
return true;
});


_androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
Expand All @@ -110,13 +132,15 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
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 -> {
selectBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
return false;
});

Expand Down Expand Up @@ -158,25 +182,28 @@ private void onSelectBackupsLocationResult(int resultCode, Intent data) {

_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() {
boolean encrypted = _vaultManager.getVault().isEncryptionEnabled();
boolean androidBackupEnabled = _prefs.isAndroidBackupsEnabled() && encrypted;
boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted;
boolean backupReminderEnabled = _prefs.isBackupReminderEnabled();
boolean isSingleBackupEnabled = _prefs.isSingleBackupEnabled();
_backupsPasswordWarningPreference.setVisible(_vaultManager.getVault().isBackupPasswordSet());
_androidBackupsPreference.setChecked(androidBackupEnabled);
_androidBackupsPreference.setEnabled(encrypted);
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled);
_versioningStrategyPreference.setVisible(backupEnabled);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled && !isSingleBackupEnabled);
if (backupEnabled) {
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
}
Expand Down Expand Up @@ -221,6 +248,14 @@ private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult r
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
Expand All @@ -241,6 +276,31 @@ private void scheduleBackup() {
}
}

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 = null;
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);
}
if (text == null) {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,6 +10,7 @@
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;
Expand Down Expand Up @@ -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;
Expand All @@ -51,10 +54,10 @@ public VaultBackupManager(Context context, AuditLogRepository auditLogRepository
_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) {
Expand All @@ -64,6 +67,46 @@ public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
});
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
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;
Expand Down Expand Up @@ -172,8 +174,11 @@ public void scheduleBackup() throws VaultRepositoryException {
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);
}
Expand Down
Loading

0 comments on commit 568683a

Please sign in to comment.