diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 559d0315..001c8dfc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
_keyProfiles;
@@ -140,6 +143,54 @@ public class MainActivity extends AppCompatActivity {
case CODE_IMPORT:
onImportResult(resultCode, data);
break;
+ case CODE_PREFERENCES:
+ onPreferencesResult(resultCode, data);
+ break;
+ }
+ }
+
+ private void onPreferencesResult(int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ return;
+ }
+
+ // TODO: create a custom layout to show a message AND a checkbox
+ int action = data.getIntExtra("action", -1);
+ switch (action) {
+ case PreferencesActivity.ACTION_EXPORT:
+ final boolean[] checked = {true};
+ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
+ .setTitle("Export the database")
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ String filename;
+ try {
+ filename = _db.export(checked[0]);
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "An error occurred while trying to export the database", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // make sure the new file is visible
+ MediaScannerConnection.scanFile(this, new String[]{filename}, null, null);
+
+ Toast.makeText(this, "The database has been exported to: " + filename, Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton(android.R.string.cancel, null);
+ if (_db.getFile().isEncrypted()) {
+ final String[] items = {"Keep the database encrypted"};
+ final boolean[] checkedItems = {true};
+ builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int index, boolean isChecked) {
+ checked[0] = isChecked;
+ }
+ });
+ } else {
+ builder.setMessage("This action will export the database out of Android's private storage.");
+ }
+ builder.show();
+ break;
}
}
@@ -371,7 +422,7 @@ public class MainActivity extends AppCompatActivity {
switch (item.getItemId()) {
case R.id.action_settings:
Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
- startActivity(preferencesActivity);
+ startActivityForResult(preferencesActivity, CODE_PREFERENCES);
return true;
case R.id.action_import:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
diff --git a/app/src/main/java/me/impy/aegis/PreferencesActivity.java b/app/src/main/java/me/impy/aegis/PreferencesActivity.java
index 071bd72d..147c4530 100644
--- a/app/src/main/java/me/impy/aegis/PreferencesActivity.java
+++ b/app/src/main/java/me/impy/aegis/PreferencesActivity.java
@@ -1,5 +1,7 @@
package me.impy.aegis;
+import android.app.Activity;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
@@ -9,13 +11,14 @@ import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
public class PreferencesActivity extends AppCompatActivity {
+ public static final int ACTION_EXPORT = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- SharedPreferences mySharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- if (mySharedPreferences.getBoolean("pref_night_mode", false)) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ if (preferences.getBoolean("pref_night_mode", false)) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme_Default);
@@ -29,7 +32,6 @@ public class PreferencesActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
addPreferencesFromResource(R.xml.preferences);
final Preference nightModePreference = findPreference("pref_night_mode");
@@ -40,6 +42,20 @@ public class PreferencesActivity extends AppCompatActivity {
return true;
}
});
+
+ Preference exportPreference = findPreference("pref_export");
+ exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent();
+ intent.putExtra("action", ACTION_EXPORT);
+
+ Activity activity = getActivity();
+ activity.setResult(RESULT_OK, intent);
+ activity.finish();
+ return true;
+ }
+ });
}
}
}
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
index 2d3730da..07beb9ce 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java
@@ -5,6 +5,7 @@ import android.content.Context;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
+import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -116,37 +117,6 @@ public class DatabaseFile {
return !_slots.isEmpty() && _cryptParameters != null;
}
- public void save(Context context, String filename) throws IOException {
- byte[] data = serialize();
-
- FileOutputStream file = context.openFileOutput(filename, Context.MODE_PRIVATE);
- file.write(data);
- file.close();
- }
-
- public static DatabaseFile load(Context context, String filename) throws Exception {
- byte[] bytes;
- FileInputStream file = null;
-
- try {
- file = context.openFileInput(filename);
- DataInputStream stream = new DataInputStream(file);
- bytes = new byte[(int) file.getChannel().size()];
- stream.readFully(bytes);
- stream.close();
- } finally {
- // always close the file
- // there is no need to close the DataInputStream
- if (file != null) {
- file.close();
- }
- }
-
- DatabaseFile db = new DatabaseFile();
- db.deserialize(bytes);
- return db;
- }
-
private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException {
stream.write(id);
diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
index 878c4643..31733cdc 100644
--- a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
+++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java
@@ -1,16 +1,23 @@
package me.impy.aegis.db;
import android.content.Context;
+import android.os.Environment;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.util.List;
-import me.impy.aegis.KeyProfile;
import me.impy.aegis.crypto.CryptParameters;
import me.impy.aegis.crypto.CryptResult;
import me.impy.aegis.crypto.MasterKey;
public class DatabaseManager {
- public static final String FILENAME = "aegis.db";
+ private static final String FILENAME = "aegis.db";
+ private static final String FILENAME_EXPORT = "aegis_export.db";
+ private static final String FILENAME_EXPORT_PLAIN = "aegis_export.json";
private MasterKey _key;
private DatabaseFile _file;
@@ -22,11 +29,30 @@ public class DatabaseManager {
}
public void load() throws Exception {
- _file = DatabaseFile.load(_context, FILENAME);
+ byte[] fileBytes;
+ FileInputStream file = null;
+
+ try {
+ file = _context.openFileInput(FILENAME);
+ fileBytes = new byte[(int) file.getChannel().size()];
+ DataInputStream stream = new DataInputStream(file);
+ stream.readFully(fileBytes);
+ stream.close();
+ } finally {
+ // always close the file stream
+ // there is no need to close the DataInputStream
+ if (file != null) {
+ file.close();
+ }
+ }
+
+ _file = new DatabaseFile();
+ _file.deserialize(fileBytes);
+
if (!_file.isEncrypted()) {
- byte[] bytes = _file.getContent();
+ byte[] contentBytes = _file.getContent();
_db = new Database();
- _db.deserialize(bytes);
+ _db.deserialize(contentBytes);
}
}
@@ -40,17 +66,64 @@ public class DatabaseManager {
_key = key;
}
+ public static void save(Context context, DatabaseFile file) throws IOException {
+ byte[] bytes = file.serialize();
+
+ FileOutputStream stream = null;
+ try {
+ stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE);
+ stream.write(bytes);
+ } finally {
+ // always close the file stream
+ if (stream != null) {
+ stream.close();
+ }
+ }
+ }
+
public void save() throws Exception {
assertDecrypted();
- byte[] bytes = _db.serialize();
+ byte[] dbBytes = _db.serialize();
if (!_file.isEncrypted()) {
- _file.setContent(bytes);
+ _file.setContent(dbBytes);
} else {
+ CryptResult result = _key.encrypt(dbBytes);
+ _file.setContent(result.Data);
+ _file.setCryptParameters(result.Parameters);
+ }
+ save(_context, _file);
+ }
+
+ public String export(boolean encrypt) throws Exception {
+ assertDecrypted();
+ byte[] bytes = _db.serialize();
+ encrypt = encrypt && getFile().isEncrypted();
+ if (encrypt) {
CryptResult result = _key.encrypt(bytes);
_file.setContent(result.Data);
_file.setCryptParameters(result.Parameters);
+ bytes = _file.serialize();
+ }
+
+ File file;
+ FileOutputStream stream = null;
+ try {
+ File dir = new File(Environment.getExternalStorageDirectory(), "Aegis");
+ if (!dir.exists() && !dir.mkdirs()) {
+ throw new IOException("error creating external storage directory");
+ }
+
+ file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
+ stream = new FileOutputStream(file);
+ stream.write(bytes);
+ } finally {
+ // always close the file stream
+ if (stream != null) {
+ stream.close();
+ }
}
- _file.save(_context, FILENAME);
+
+ return file.getAbsolutePath();
}
public void addKey(DatabaseEntry entry) throws Exception {
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 8b14fedb..0eb2e2c0 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -13,4 +13,10 @@
android:key="pref_issuer"
android:title="@string/pref_issuers"
android:summary="@string/pref_issuers_description"/>
-
\ No newline at end of file
+
+
+
+