Rewrite the export functionality to use the Storage Access Framework

We noticed after the v1.1 release that we need to switch our external storage
access code over to use the Storage Access Framework. The export functionality
was the only piece of code that still used the deprecated method and this patch
addresses that.

The Storage Access Framework is a little annoying to work with, but I don't
think it'll be too painful for our usecase. This switch comes with some nice
benefits for users as well. Users are now able to select a custom storage
location, which can be local or in the cloud.

This also removes ``requestLegacyExternalStorage`` from the manifest, as we
don't need it anymore.
This commit is contained in:
Alexander Bakker 2020-01-04 18:40:25 +01:00
parent 10c206270a
commit 1ede56e137
4 changed files with 46 additions and 43 deletions

View file

@ -19,8 +19,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme"
android:requestLegacyExternalStorage="true">
tools:replace="android:theme">
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about"></activity>

View file

@ -5,7 +5,6 @@ import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import android.view.Window;
@ -26,16 +25,6 @@ import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
@ -47,6 +36,16 @@ import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -54,6 +53,7 @@ import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -70,6 +70,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private static final int CODE_SLOTS = 2;
private static final int CODE_GROUPS = 3;
private static final int CODE_SELECT_ENTRIES = 4;
private static final int CODE_EXPORT = 5;
private static final int CODE_EXPORT_ENCRYPT = 6;
// permission request codes
private static final int CODE_PERM_IMPORT = 0;
@ -406,6 +408,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_ENCRYPT:
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
break;
}
}
@ -587,18 +594,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename;
try {
filename = _vault.export(checked.get());
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN);
// make sure the new file is visible
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
Toast.makeText(getActivity(), getString(R.string.export_vault_location) + filename, Toast.LENGTH_SHORT).show();
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
})
.setNegativeButton(android.R.string.cancel, null);
if (_vault.isEncryptionEnabled()) {
@ -669,6 +670,22 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRecreate", true);
}
private void onExportResult(int resultCode, Intent data, boolean encrypt) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
_vault.export(stream, encrypt);
} catch (IOException | VaultManagerException e) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
private boolean saveVault() {
try {
_vault.save();

View file

@ -2,10 +2,7 @@ package com.beemdevelopment.aegis.vault;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.services.NotificationService;
import org.json.JSONObject;
@ -15,14 +12,15 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.Collator;
import java.util.Collection;
import java.util.TreeSet;
public class VaultManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
public static final String FILENAME_EXPORT = "aegis_export";
public static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain";
private Vault _vault;
private VaultFile _file;
@ -104,7 +102,7 @@ public class VaultManager {
}
}
public String export(boolean encrypt) throws VaultManagerException {
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
assertState(false, true);
try {
@ -115,19 +113,8 @@ public class VaultManager {
vaultFile.setContent(_vault.toJson());
}
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("error creating external storage directory");
}
byte[] bytes = vaultFile.toBytes();
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
try (FileOutputStream stream = new FileOutputStream(file)) {
stream.write(bytes);
}
return file.getAbsolutePath();
} catch (IOException | VaultFileException e) {
throw new VaultManagerException(e);
}

View file

@ -127,7 +127,7 @@
<string name="read_entries_count">Read %d entries. %d errors.</string>
<string name="import_error_title">One or more errors occurred during the import</string>
<string name="exporting_vault_error">An error occurred while trying to export the vault</string>
<string name="export_vault_location">The vault has been exported to:</string>
<string name="exported_vault">The vault has been exported</string>
<string name="export_warning">This action will export the vault out of Aegis\' private storage.</string>
<string name="encryption_set_password_error">An error occurred while trying to set the password: </string>
<string name="encryption_enable_biometrics_error">An error occurred while trying to enable biometric unlock</string>