Build a structure to allow for more easily creating new versions of

preferences Storage importers/exporters.  Password/encryption key
prompting is now down in centralized place.  On import, the password
prompt is given if the file to be imported uses an importer
implementation that requires a password and no password is provided.
On export, the password prompt is given if the chosen version is for
an exporter that requires a password and no password was provided.

For instance, for automatic backups, a password could be stored in
preferences and provided to the exporter, so no password prompt would
be given.
This commit is contained in:
danapple 2011-03-20 11:52:13 -05:00
parent 19bff64672
commit 89bdbdce94
16 changed files with 731 additions and 311 deletions

View file

@ -1039,12 +1039,14 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="settings_import_success_multiple">Imported <xliff:g id="numAccounts">%s</xliff:g> accounts from <xliff:g id="filename">%s</xliff:g></string>
<string name="settings_import_success_single">Imported 1 account from <xliff:g id="filename">%s</xliff:g></string>
<string name="settings_export_failure">Failed to export settings: <xliff:g id="reason">%s</xliff:g></string>
<string name="settings_import_failure">Failed from import settings from <xliff:g id="filename">%s</xliff:g>:<xliff:g id="reason">%s</xliff:g></string>
<string name="settings_import_failure">Failed to import settings from <xliff:g id="filename">%s</xliff:g>:<xliff:g id="reason">%s</xliff:g></string>
<string name="settings_export_success_header">Export succeeded</string>
<string name="settings_export_failed_header">Export failed</string>
<string name="settings_import_success_header">Import succeeded</string>
<string name="settings_import_failed_header">Import failed</string>
<string name="settings_unknown_version">Unable to handle file of version <xliff:g id="version">%s</xliff:g></string>
<string name="account_unavailable">Account \"<xliff:g id="account">%s</xliff:g>\" is unavailable; check storage</string>
</resources>

View file

@ -1,7 +1,16 @@
package com.fsck.k9.activity;
import android.app.Activity;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentResolver;
@ -15,31 +24,46 @@ import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.TypedValue;
import android.view.*;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.*;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import com.fsck.k9.*;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
import com.fsck.k9.BaseAccount;
import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.AccountSetupBasics;
import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.StorageManager;
import com.fsck.k9.view.ColorChip;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class Accounts extends K9ListActivity implements OnItemClickListener, OnClickListener {
/**
@ -832,70 +856,78 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
private void onImport(Uri uri) {
Log.i(K9.LOG_TAG, "onImport importing from URI " + uri.getPath());
try {
final String fileName = uri.getPath();
ContentResolver resolver = getContentResolver();
final InputStream is = resolver.openInputStream(uri);
PasswordEntryDialog dialog = new PasswordEntryDialog(this, getString(R.string.settings_encryption_password_prompt),
new PasswordEntryDialog.PasswordEntryListener() {
public void passwordChosen(String chosenPassword) {
String toastText = Accounts.this.getString(R.string.settings_importing);
Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, Toast.LENGTH_SHORT);
toast.show();
mHandler.progress(true);
AsyncUIProcessor.getInstance(Accounts.this.getApplication()).importSettings(is, chosenPassword, new ImportListener() {
public void failure(final String message, Exception e) {
Accounts.this.runOnUiThread(new Runnable() {
public void run() {
mHandler.progress(false);
showDialog(Accounts.this, R.string.settings_import_failed_header, Accounts.this.getString(R.string.settings_import_failure, fileName, message));
}
});
}
public void importSuccess(final int numAccounts) {
Accounts.this.runOnUiThread(new Runnable() {
public void run() {
mHandler.progress(false);
String messageText =
numAccounts != 1
? Accounts.this.getString(R.string.settings_import_success_multiple, numAccounts, fileName)
: Accounts.this.getString(R.string.settings_import_success_single, fileName);
showDialog(Accounts.this, R.string.settings_import_success_header, messageText);
refresh();
}
});
}
});
}
public void cancel() {
}
});
dialog.show();
} catch (FileNotFoundException fnfe) {
String toastText = Accounts.this.getString(R.string.settings_import_failure, uri.getPath(), fnfe.getMessage());
Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1);
toast.show();
}
}
private static void showDialog(final Activity activity, int headerRes, String message) {
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(headerRes);
builder.setMessage(message);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
final String fileName = uri.getPath();
AsyncUIProcessor.getInstance(Accounts.this.getApplication()).importSettings(this, uri, new ImportListener()
{
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
public void success(int numAccounts)
{
mHandler.progress(false);
String messageText =
numAccounts != 1
? Accounts.this.getString(R.string.settings_import_success_multiple, numAccounts, fileName)
: Accounts.this.getString(R.string.settings_import_success_single, fileName);
showDialog(Accounts.this, R.string.settings_import_success_header, messageText);
runOnUiThread(new Runnable() {
@Override
public void run()
{
refresh();
}
});
}
@Override
public void failure(String message, Exception e)
{
mHandler.progress(false);
showDialog(Accounts.this, R.string.settings_import_failed_header, Accounts.this.getString(R.string.settings_import_failure, fileName, e.getLocalizedMessage()));
}
@Override
public void canceled()
{
mHandler.progress(false);
}
@Override
public void started()
{
runOnUiThread(new Runnable() {
@Override
public void run()
{
mHandler.progress(true);
String toastText = Accounts.this.getString(R.string.settings_importing);
Toast toast = Toast.makeText(Accounts.this, toastText, Toast.LENGTH_SHORT);
toast.show();
}
});
}
});
builder.show();
}
private void showDialog(final Context context, final int headerRes, final String message) {
this.runOnUiThread(new Runnable() {
@Override
public void run()
{
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(headerRes);
builder.setMessage(message);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.show();
}
});
}
class AccountsAdapter extends ArrayAdapter<BaseAccount> {
public AccountsAdapter(BaseAccount[] accounts) {
super(Accounts.this, 0, accounts);

View file

@ -5,8 +5,12 @@ import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.app.Activity;
import android.app.Application;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.helper.Utility;
@ -33,7 +37,10 @@ public class AsyncUIProcessor {
}
return inst;
}
public void exportSettings(final String uuid, final String encryptionKey, final ExportListener listener) {
public void execute(Runnable runnable) {
threadPool.execute(runnable);
}
public void exportSettings(final Activity activity, final String version, final String uuid, final ExportListener listener) {
threadPool.execute(new Runnable() {
@Override
@ -46,11 +53,9 @@ public class AsyncUIProcessor {
dir.mkdirs();
File file = Utility.createUniqueFile(dir, "settings.k9s");
String fileName = file.getAbsolutePath();
StorageExporter.exportPreferences(mApplication, uuid, fileName, encryptionKey);
if (listener != null) {
listener.exportSuccess(fileName);
}
StorageExporter.exportPreferences(activity, version, uuid, fileName, null, listener);
} catch (Exception e) {
Log.w(K9.LOG_TAG, "Exception during export", e);
listener.failure(e.getLocalizedMessage(), e);
}
}
@ -58,43 +63,73 @@ public class AsyncUIProcessor {
);
}
public void importSettings(final String fileName, final String encryptionKey, final ImportListener listener) {
public void importSettings(final Activity activity, final Uri uri, final ImportListener listener) {
threadPool.execute(new Runnable() {
@Override
public void run() {
InputStream is = null;
try {
int numAccounts = StorageImporter.importPreferences(mApplication, fileName, encryptionKey);
K9.setServicesEnabled(mApplication);
if (listener != null) {
listener.importSuccess(numAccounts);
}
} catch (Exception e) {
listener.failure(e.getLocalizedMessage(), e);
ContentResolver resolver = mApplication.getContentResolver();
is = resolver.openInputStream(uri);
}
catch (Exception e) {
Log.w(K9.LOG_TAG, "Exception while resolving Uri to InputStream", e);
if (listener != null) {
listener.failure(e.getLocalizedMessage(), e);
}
return;
}
final InputStream myIs = is;
StorageImporter.importPreferences(activity, is, null, new ImportListener() {
@Override
public void failure(String message, Exception e) {
quietClose(myIs);
if (listener != null) {
listener.failure(message, e);
}
}
@Override
public void success(int numAccounts) {
quietClose(myIs);
if (listener != null) {
listener.success(numAccounts);
}
}
@Override
public void canceled() {
quietClose(myIs);
if (listener != null) {
listener.canceled();
}
}
@Override
public void started()
{
if (listener != null) {
listener.started();
}
}
});
}
}
);
);
}
public void importSettings(final InputStream inputStream, final String encryptionKey, final ImportListener listener) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
int numAccounts = StorageImporter.importPreferences(mApplication, inputStream, encryptionKey);
K9.setServicesEnabled(mApplication);
if (listener != null) {
listener.importSuccess(numAccounts);
}
} catch (Exception e) {
listener.failure(e.getLocalizedMessage(), e);
}
private void quietClose(InputStream is)
{
if (is != null) {
try {
is.close();
}
catch (Exception e) {
Log.w(K9.LOG_TAG, "Unable to close inputStream", e);
}
}
);
}
}

View file

@ -7,59 +7,90 @@ import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.R;
import com.fsck.k9.preferences.StorageVersioning;
public class ExportHelper {
public static void exportSettings(final Activity activity, final Progressable progressable, final Account account) {
PasswordEntryDialog dialog = new PasswordEntryDialog(activity, activity.getString(R.string.settings_encryption_password_prompt),
new PasswordEntryDialog.PasswordEntryListener() {
public void passwordChosen(String chosenPassword) {
String toastText = activity.getString(R.string.settings_exporting);
Toast toast = Toast.makeText(activity, toastText, Toast.LENGTH_SHORT);
toast.show();
progressable.setProgress(true);
String uuid = null;
if (account != null) {
uuid = account.getUuid();
}
AsyncUIProcessor.getInstance(activity.getApplication()).exportSettings(uuid, chosenPassword,
new ExportListener() {
public void failure(final String message, Exception e) {
activity.runOnUiThread(new Runnable() {
public void run() {
progressable.setProgress(false);
showDialog(activity, R.string.settings_export_failed_header, activity.getString(R.string.settings_export_failure, message));
}
});
}
public static void exportSettings(final Activity activity, final Account account, final ExportListener listener) {
// Once there are more versions, build a UI to select which one to use. For now, use the encrypted/encoded version:
String version = StorageVersioning.STORAGE_VERSION.VERSION1.getVersionString();
String uuid = null;
if (account != null) {
uuid = account.getUuid();
}
AsyncUIProcessor.getInstance(activity.getApplication()).exportSettings(activity, version, uuid, new ExportListener() {
public void exportSuccess(final String fileName) {
activity.runOnUiThread(new Runnable() {
public void run() {
progressable.setProgress(false);
showDialog(activity, R.string.settings_export_success_header, activity.getString(R.string.settings_export_success, fileName));
}
});
@Override
public void canceled()
{
if (listener != null) {
listener.canceled();
}
}
@Override
public void failure(String message, Exception e)
{
if (listener != null) {
listener.failure(message, e);
}
showDialog(activity, R.string.settings_export_failed_header, activity.getString(R.string.settings_export_failure, message));
}
@Override
public void started()
{
if (listener != null) {
listener.started();
}
activity.runOnUiThread(new Runnable() {
@Override
public void run()
{
String toastText = activity.getString(R.string.settings_exporting);
Toast toast = Toast.makeText(activity, toastText, Toast.LENGTH_SHORT);
toast.show();
}
});
}
public void cancel() {
}
});
dialog.show();
}
private static void showDialog(final Activity activity, int headerRes, String message) {
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(headerRes);
builder.setMessage(message);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
public void success(String fileName)
{
if (listener != null) {
listener.success(fileName);
}
showDialog(activity, R.string.settings_export_success_header, activity.getString(R.string.settings_export_success, fileName));
}
@Override
public void success()
{
// This one should never be called here because the AsyncUIProcessor will generate a filename
}
});
}
private static void showDialog(final Activity activity, final int headerRes, final String message) {
activity.runOnUiThread(new Runnable() {
builder.show();
@Override
public void run()
{
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(headerRes);
builder.setMessage(message);
builder.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.show();
}
});
}
}

View file

@ -1,8 +1,13 @@
package com.fsck.k9.activity;
public interface ExportListener {
public void exportSuccess(String fileName);
public void success(String fileName);
public void success();
public void failure(String message, Exception e);
public void canceled();
public void started();
}

View file

@ -1,8 +1,12 @@
package com.fsck.k9.activity;
public interface ImportListener {
public void importSuccess(int numAccounts);
public void success(int numAccounts);
public void failure(String message, Exception e);
public void canceled();
public void started();
}

View file

@ -21,7 +21,7 @@ import com.fsck.k9.K9;
import com.fsck.k9.helper.DateFormatter;
public class K9Activity extends Activity implements Progressable {
public class K9Activity extends Activity {
private GestureDetector gestureDetector;
protected ScrollView mTopView;
@ -167,7 +167,39 @@ public class K9Activity extends Activity implements Progressable {
}
public void onExport(final Account account) {
ExportHelper.exportSettings(this, this, account);
ExportHelper.exportSettings(this, account, new ExportListener()
{
@Override
public void canceled()
{
setProgress(false);
}
@Override
public void failure(String message, Exception e)
{
setProgress(false);
}
@Override
public void started()
{
setProgress(true);
}
@Override
public void success(String fileName)
{
setProgress(false);
}
@Override
public void success()
{
setProgress(false);
}
});
}
}

View file

@ -11,7 +11,7 @@ import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.helper.DateFormatter;
public class K9ListActivity extends ListActivity implements Progressable {
public class K9ListActivity extends ListActivity {
@Override
public void onCreate(Bundle icicle) {
K9Activity.setLanguage(this, K9.getK9Language());
@ -94,7 +94,39 @@ public class K9ListActivity extends ListActivity implements Progressable {
}
public void onExport(final Account account) {
ExportHelper.exportSettings(this, this, account);
ExportHelper.exportSettings(this, account, new ExportListener()
{
@Override
public void canceled()
{
setProgress(false);
}
@Override
public void failure(String message, Exception e)
{
setProgress(false);
}
@Override
public void started()
{
setProgress(true);
}
@Override
public void success(String fileName)
{
setProgress(false);
}
@Override
public void success()
{
setProgress(false);
}
});
}
}

View file

@ -1,5 +0,0 @@
package com.fsck.k9.activity;
public interface Progressable {
public void setProgress(boolean progress);
}

View file

@ -0,0 +1,11 @@
package com.fsck.k9.preferences;
import java.io.OutputStream;
import android.content.Context;
public interface IStorageExporter
{
public boolean needsKey();
public void exportPreferences(Context context, String uuid, OutputStream os, String encryptionKey) throws StorageImportExportException;
}

View file

@ -1,9 +1,11 @@
package com.fsck.k9.preferences;
import com.fsck.k9.Preferences;
import com.fsck.k9.preferences.StorageImporter.ImportElement;
import android.content.SharedPreferences;
public interface IStorageImporter {
public abstract int importPreferences(Preferences preferences, SharedPreferences.Editor context, String data, String encryptionKey) throws StorageImportExportException;
public boolean needsKey();
public abstract int importPreferences(Preferences preferences, SharedPreferences.Editor context, ImportElement dataset, String encryptionKey) throws StorageImportExportException;
}

View file

@ -1,112 +1,134 @@
package com.fsck.k9.preferences;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.content.Context;
import android.content.SharedPreferences;
import android.app.Activity;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.AsyncUIProcessor;
import com.fsck.k9.activity.ExportListener;
import com.fsck.k9.activity.PasswordEntryDialog;
public class StorageExporter {
public static void exportPreferences(Context context, String uuid, String fileName, String encryptionKey) throws StorageImportExportException {
Log.i(K9.LOG_TAG, "Exporting preferences for account " + uuid + " to file " + fileName);
File outFile = new File(fileName);
OutputStream os = null;
public class StorageExporter
{
private static void exportPreferences(Activity activity, String version, String uuid, String fileName, OutputStream os, String encryptionKey, final ExportListener listener) {
try {
os = new FileOutputStream(outFile);
} catch (FileNotFoundException e) {
throw new StorageImportExportException("Unable to export settings", e);
IStorageExporter storageExporter = StorageVersioning.createExporter(version);
if (storageExporter == null) {
throw new StorageImportExportException(activity.getString(R.string.settings_unknown_version, version), null);
}
if (storageExporter.needsKey() && encryptionKey == null) {
gatherPassword(activity, storageExporter, uuid, fileName, os, listener);
}
else
{
finishExport(activity, storageExporter, uuid, fileName, os, encryptionKey, listener);
}
}
catch (Exception e)
{
if (listener != null) {
listener.failure(e.getLocalizedMessage(), e);
}
}
}
public static void exportPreferences(Activity activity, String version, String uuid, String fileName, String encryptionKey, final ExportListener listener) throws StorageImportExportException {
exportPreferences(activity, version, uuid, fileName, null, encryptionKey, listener);
}
public static void exportPrefererences(Activity activity, String version, String uuid, OutputStream os, String encryptionKey, final ExportListener listener) throws StorageImportExportException {
exportPreferences(activity, version, uuid, null, os, encryptionKey, listener);
}
private static void gatherPassword(final Activity activity, final IStorageExporter storageExporter, final String uuid, final String fileName, final OutputStream os, final ExportListener listener) {
activity.runOnUiThread(new Runnable() {
@Override
public void run()
{
PasswordEntryDialog dialog = new PasswordEntryDialog(activity, activity.getString(R.string.settings_encryption_password_prompt),
new PasswordEntryDialog.PasswordEntryListener() {
public void passwordChosen(final String chosenPassword) {
AsyncUIProcessor.getInstance(activity.getApplication()).execute(new Runnable() {
@Override
public void run()
{
try {
finishExport(activity, storageExporter, uuid, fileName, os, chosenPassword, listener);
}
catch (Exception e) {
Log.w(K9.LOG_TAG, "Exception while finishing export", e);
if (listener != null) {
listener.failure(e.getLocalizedMessage(), e);
}
}
}
});
}
public void cancel() {
if (listener != null) {
listener.canceled();
}
}
});
dialog.show();
}
});
}
private static void finishExport(Activity activity, IStorageExporter storageExporter, String uuid, String fileName, OutputStream os, String encryptionKey, ExportListener listener) throws StorageImportExportException {
boolean needToClose = false;
if (listener != null) {
listener.started();
}
try {
exportPrefererences(context, uuid, os, encryptionKey);
} finally {
// This needs to be after the password prompt. If the user cancels the password, we do not want
// to create the file needlessly
if (os == null && fileName != null) {
needToClose = true;
File outFile = new File(fileName);
os = new FileOutputStream(outFile);
}
if (os != null) {
storageExporter.exportPreferences(activity, uuid, os, encryptionKey);
if (listener != null) {
if (fileName != null) {
listener.success(fileName);
}
else {
listener.success();
}
}
}
else {
throw new StorageImportExportException("Internal error; no fileName or OutputStream", null);
}
}
catch (Exception e) {
throw new StorageImportExportException(e.getLocalizedMessage(), e);
}
finally {
if (needToClose && os != null) {
try {
os.close();
} catch (Exception e) {
Log.i(K9.LOG_TAG, "Unable to close OutputStream for file " + fileName + ": " + e.getLocalizedMessage());
}
catch (Exception e) {
Log.w(K9.LOG_TAG, "Unable to close OutputStream", e);
}
}
}
Log.i(K9.LOG_TAG, "Exported preferences for account " + uuid + " to file " + fileName + " which is size " + outFile.length());
}
public static void exportPrefererences(Context context, String uuid, OutputStream os, String encryptionKey) throws StorageImportExportException {
try {
Log.i(K9.LOG_TAG, "Exporting preferences for account " + uuid + " to OutputStream");
K9Krypto krypto = new K9Krypto(encryptionKey, K9Krypto.MODE.ENCRYPT);
OutputStreamWriter sw = new OutputStreamWriter(os);
PrintWriter pf = new PrintWriter(sw);
long keysEvaluated = 0;
long keysExported = 0;
pf.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
pf.print("<k9settings version=\"1\"");
pf.println(">");
Preferences preferences = Preferences.getPreferences(context);
SharedPreferences storage = preferences.getPreferences();
Account[] accounts = preferences.getAccounts();
Set<String> accountUuids = new HashSet<String>();
for (Account account : accounts) {
accountUuids.add(account.getUuid());
}
Map < String, ? extends Object > prefs = storage.getAll();
for (Map.Entry < String, ? extends Object > entry : prefs.entrySet()) {
String key = entry.getKey();
String value = entry.getValue().toString();
//Log.i(K9.LOG_TAG, "Evaluating key " + key);
keysEvaluated++;
if (uuid != null) {
String[] comps = key.split("\\.");
String keyUuid = comps[0];
//Log.i(K9.LOG_TAG, "Got key uuid " + keyUuid);
if (uuid.equals(keyUuid) == false) {
//Log.i(K9.LOG_TAG, "Skipping key " + key + " which is for another account or global");
continue;
}
} else {
String[] comps = key.split("\\.");
if (comps.length > 1) {
String keyUuid = comps[0];
if (accountUuids.contains(keyUuid) == false) {
//Log.i(K9.LOG_TAG, "Skipping key " + key + " which is not for any current account");
continue;
}
}
}
String keyEnc = krypto.encrypt(key);
String valueEnc = krypto.encrypt(value);
String output = keyEnc + ":" + valueEnc;
//Log.i(K9.LOG_TAG, "For key " + key + ", output is " + output);
pf.println(output);
keysExported++;
}
pf.println("</k9settings>");
pf.flush();
Log.i(K9.LOG_TAG, "Exported " + keysExported + " settings of " + keysEvaluated
+ " total for preferences for account " + uuid);
} catch (Exception e) {
throw new StorageImportExportException("Unable to encrypt settings", e);
}
}
}

View file

@ -0,0 +1,92 @@
package com.fsck.k9.preferences;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
public class StorageExporterVersion1 implements IStorageExporter {
public void exportPreferences(Context context, String uuid, OutputStream os, String encryptionKey) throws StorageImportExportException {
try {
Log.i(K9.LOG_TAG, "Exporting preferences for account " + uuid + " to OutputStream");
K9Krypto krypto = new K9Krypto(encryptionKey, K9Krypto.MODE.ENCRYPT);
OutputStreamWriter sw = new OutputStreamWriter(os);
PrintWriter pf = new PrintWriter(sw);
long keysEvaluated = 0;
long keysExported = 0;
pf.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
pf.print("<k9settings version=\"1\"");
pf.println(">");
Preferences preferences = Preferences.getPreferences(context);
SharedPreferences storage = preferences.getPreferences();
Account[] accounts = preferences.getAccounts();
Set<String> accountUuids = new HashSet<String>();
for (Account account : accounts) {
accountUuids.add(account.getUuid());
}
Map < String, ? extends Object > prefs = storage.getAll();
for (Map.Entry < String, ? extends Object > entry : prefs.entrySet()) {
String key = entry.getKey();
String value = entry.getValue().toString();
//Log.i(K9.LOG_TAG, "Evaluating key " + key);
keysEvaluated++;
if (uuid != null) {
String[] comps = key.split("\\.");
String keyUuid = comps[0];
//Log.i(K9.LOG_TAG, "Got key uuid " + keyUuid);
if (uuid.equals(keyUuid) == false) {
//Log.i(K9.LOG_TAG, "Skipping key " + key + " which is for another account or global");
continue;
}
} else {
String[] comps = key.split("\\.");
if (comps.length > 1) {
String keyUuid = comps[0];
if (accountUuids.contains(keyUuid) == false) {
//Log.i(K9.LOG_TAG, "Skipping key " + key + " which is not for any current account");
continue;
}
}
}
String keyEnc = krypto.encrypt(key);
String valueEnc = krypto.encrypt(value);
String output = keyEnc + ":" + valueEnc;
//Log.i(K9.LOG_TAG, "For key " + key + ", output is " + output);
pf.println(output);
keysExported++;
}
pf.println("</k9settings>");
pf.flush();
Log.i(K9.LOG_TAG, "Exported " + keysExported + " settings of " + keysEvaluated
+ " total for preferences for account " + uuid);
} catch (Exception e) {
throw new StorageImportExportException("Unable to encrypt settings", e);
}
}
@Override
public boolean needsKey()
{
return true;
}
}

View file

@ -1,14 +1,10 @@
package com.fsck.k9.preferences;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
@ -18,43 +14,22 @@ import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import android.content.Context;
import android.app.Activity;
import android.content.SharedPreferences;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.AsyncUIProcessor;
import com.fsck.k9.activity.ImportListener;
import com.fsck.k9.activity.PasswordEntryDialog;
import com.fsck.k9.helper.DateFormatter;
public class StorageImporter {
public static int importPreferences(Context context, String fileName, String encryptionKey) throws StorageImportExportException {
InputStream is = null;
try {
is = new FileInputStream(fileName);
} catch (FileNotFoundException fnfe) {
throw new StorageImportExportException("Failure opening settings file " + fileName, fnfe);
}
public static void importPreferences(Activity activity, InputStream is, String providedEncryptionKey, ImportListener listener) {
try {
int count = importPreferences(context, is, encryptionKey);
return count;
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
Log.i(K9.LOG_TAG, "Unable to close InputStream for file " + fileName + ": " + e.getLocalizedMessage());
}
}
}
}
public static int importPreferences(Context context, InputStream is, String encryptionKey) throws StorageImportExportException {
try {
Preferences preferences = Preferences.getPreferences(context);
SharedPreferences storage = preferences.getPreferences();
SharedPreferences.Editor editor = storage.edit();
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
@ -63,48 +38,104 @@ public class StorageImporter {
xr.parse(new InputSource(is));
Element dataset = handler.getRootElement();
ImportElement dataset = handler.getRootElement();
String version = dataset.attributes.get("version");
Log.i(K9.LOG_TAG, "Got settings file version " + version);
IStorageImporter storageImporter = null;
if ("1".equals(version)) {
storageImporter = new StorageImporterVersion1();
} else {
throw new StorageImportExportException("Unable to read file of version " + version
+ "; (only version 1 is readable)");
IStorageImporter storageImporter = StorageVersioning.createImporter(version);
if (storageImporter == null)
{
throw new StorageImportExportException(activity.getString(R.string.settings_unknown_version, version));
}
int numAccounts = 0;
if (storageImporter != null) {
String data = dataset.data.toString();
numAccounts = storageImporter.importPreferences(preferences, editor, data, encryptionKey);
if (providedEncryptionKey != null || storageImporter.needsKey() == false) {
Log.i(K9.LOG_TAG, "Version " + version + " settings file needs encryption key");
finishImport(activity, storageImporter, dataset, providedEncryptionKey, listener);
}
else {
gatherPassword(activity, storageImporter, dataset, listener);
}
}
catch (Exception e)
{
if (listener != null) {
listener.failure(e.getLocalizedMessage(), e);
}
editor.commit();
Preferences.getPreferences(context).refreshAccounts();
DateFormatter.clearChosenFormat();
K9.loadPrefs(Preferences.getPreferences(context));
return numAccounts;
} catch (SAXException se) {
throw new StorageImportExportException("Failure reading settings file", se);
} catch (IOException ie) {
throw new StorageImportExportException("Failure reading settings file", ie);
} catch (ParserConfigurationException pce) {
throw new StorageImportExportException("Failure reading settings file", pce);
}
}
private static void finishImport(Activity context, IStorageImporter storageImporter, ImportElement dataset, String encryptionKey, ImportListener listener) throws StorageImportExportException {
if (listener != null) {
listener.started();
}
Preferences preferences = Preferences.getPreferences(context);
SharedPreferences storage = preferences.getPreferences();
SharedPreferences.Editor editor = storage.edit();
int numAccounts = 0;
if (storageImporter != null) {
numAccounts = storageImporter.importPreferences(preferences, editor, dataset, encryptionKey);
}
editor.commit();
Preferences.getPreferences(context).refreshAccounts();
DateFormatter.clearChosenFormat();
K9.loadPrefs(Preferences.getPreferences(context));
K9.setServicesEnabled(context);
if (listener != null) {
listener.success(numAccounts);
}
}
private static void gatherPassword(final Activity activity, final IStorageImporter storageImporter, final ImportElement dataset, final ImportListener listener) {
activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
PasswordEntryDialog dialog = new PasswordEntryDialog(activity, activity.getString(R.string.settings_encryption_password_prompt),
new PasswordEntryDialog.PasswordEntryListener() {
public void passwordChosen(final String chosenPassword) {
AsyncUIProcessor.getInstance(activity.getApplication()).execute(new Runnable() {
@Override
public void run()
{
try {
finishImport(activity, storageImporter, dataset, chosenPassword, listener);
}
catch (Exception e) {
Log.w(K9.LOG_TAG, "Failure during import", e);
if (listener != null) {
listener.failure(e.getLocalizedMessage(), e);
}
}
}
});
}
public void cancel() {
if (listener != null) {
listener.canceled();
}
}
});
dialog.show();
}
});
};
private static class Element {
public static class ImportElement {
String name;
Map<String, String> attributes = new HashMap<String, String>();
Map<String, Element> subElements = new HashMap<String, Element>();
Map<String, ImportElement> subElements = new HashMap<String, ImportElement>();
StringBuilder data = new StringBuilder();
}
private static class StorageImporterHandler extends DefaultHandler {
private Element rootElement = new Element();
private Stack<Element> mOpenTags = new Stack<Element>();
private ImportElement rootElement = new ImportElement();
private Stack<ImportElement> mOpenTags = new Stack<ImportElement>();
public Element getRootElement() {
public ImportElement getRootElement() {
return this.rootElement;
}
@ -121,7 +152,7 @@ public class StorageImporter {
public void startElement(String namespaceURI, String localName,
String qName, Attributes attributes) throws SAXException {
Log.i(K9.LOG_TAG, "Starting element " + localName);
Element element = new Element();
ImportElement element = new ImportElement();
element.name = localName;
mOpenTags.push(element);
for (int i = 0; i < attributes.getLength(); i++) {
@ -135,8 +166,8 @@ public class StorageImporter {
@Override
public void endElement(String namespaceURI, String localName, String qName) {
Log.i(K9.LOG_TAG, "Ending element " + localName);
Element element = mOpenTags.pop();
Element superElement = mOpenTags.empty() ? null : mOpenTags.peek();
ImportElement element = mOpenTags.pop();
ImportElement superElement = mOpenTags.empty() ? null : mOpenTags.peek();
if (superElement != null) {
superElement.subElements.put(element.name, element);
} else {

View file

@ -14,10 +14,13 @@ import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.preferences.StorageImporter.ImportElement;
public class StorageImporterVersion1 implements IStorageImporter {
public int importPreferences(Preferences preferences, SharedPreferences.Editor editor, String data, String encryptionKey) throws StorageImportExportException {
public int importPreferences(Preferences preferences, SharedPreferences.Editor editor, ImportElement dataset, String encryptionKey) throws StorageImportExportException {
try {
String data = dataset.data.toString();
List<Integer> accountNumbers = Account.getExistingAccountNumbers(preferences);
Log.i(K9.LOG_TAG, "Existing accountNumbers = " + accountNumbers);
Map<String, String> uuidMapping = new HashMap<String, String>();
@ -83,4 +86,10 @@ public class StorageImporterVersion1 implements IStorageImporter {
throw new StorageImportExportException("Unable to decrypt settings", e);
}
}
@Override
public boolean needsKey()
{
return true;
}
}

View file

@ -0,0 +1,85 @@
package com.fsck.k9.preferences;
import java.util.HashMap;
import java.util.Map;
public class StorageVersioning
{
public enum STORAGE_VERSION {
VERSION1(StorageImporterVersion1.class, StorageExporterVersion1.class, true, STORAGE_VERSION_1);
private Class<? extends IStorageImporter> importerClass;
private Class<? extends IStorageExporter> exporterClass;
private boolean needsKey;
private String versionString;
private STORAGE_VERSION(Class<? extends IStorageImporter> imclass, Class<? extends IStorageExporter> exclass, boolean nk, String vs) {
importerClass = imclass;
exporterClass = exclass;
needsKey = nk;
versionString = vs;
}
public Class<? extends IStorageImporter> getImporterClass() {
return importerClass;
}
public IStorageImporter createImporter() throws InstantiationException, IllegalAccessException {
IStorageImporter storageImporter = importerClass.newInstance();
return storageImporter;
}
public Class<? extends IStorageExporter> getExporterClass() {
return exporterClass;
}
public IStorageExporter createExporter() throws InstantiationException, IllegalAccessException {
IStorageExporter storageExporter = exporterClass.newInstance();
return storageExporter;
}
public boolean needsKey() {
return needsKey;
}
public String getVersionString() {
return versionString;
}
}
// Never, ever re-use these numbers!
private static final String STORAGE_VERSION_1 = "1";
public static Map<String, STORAGE_VERSION> versionMap = new HashMap<String, STORAGE_VERSION>();
static {
versionMap.put(STORAGE_VERSION.VERSION1.getVersionString(), STORAGE_VERSION.VERSION1);
}
public static IStorageImporter createImporter(String version) throws InstantiationException, IllegalAccessException
{
STORAGE_VERSION storageVersion = versionMap.get(version);
if (storageVersion == null)
{
return null;
}
return storageVersion.createImporter();
}
public static IStorageExporter createExporter(String version) throws InstantiationException, IllegalAccessException
{
STORAGE_VERSION storageVersion = versionMap.get(version);
if (storageVersion == null)
{
return null;
}
return storageVersion.createExporter();
}
public Boolean needsKey(String version)
{
STORAGE_VERSION storageVersion = versionMap.get(version);
if (storageVersion == null)
{
return null;
}
return storageVersion.needsKey();
}
}