First mostly working copy of export/import. Committing while I sort

out how to use git.
This commit is contained in:
danapple 2011-02-26 11:31:56 -06:00
parent c1df45414f
commit 6dc94fb78e
26 changed files with 1101 additions and 72 deletions

View File

@ -67,7 +67,7 @@
<activity
android:name="com.fsck.k9.activity.Accounts"
android:launchMode="singleInstance"
android:launchMode="singleTask"
android:configChanges="locale"
android:label="@string/app_name">
<intent-filter>
@ -269,6 +269,10 @@
android:name="com.fsck.k9.activity.AccessibleEmailContentActivity"
>
</activity>
<activity
android:name="com.fsck.k9.activity.ImportSettingsActivity"
>
</activity>
<receiver android:name="com.fsck.k9.service.BootReceiver"
android:enabled="true"

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
>
<EditText
android:id="@+id/password_text_box"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dip"
android:layout_marginRight="6dip"
android:inputType="textPassword"
android:imeOptions="actionNext"
android:singleLine="true"
android:textColor="@android:color/primary_text_light"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>

View File

@ -6,8 +6,17 @@
android:title="@string/check_mail_action" />
<item android:id="@+id/empty_trash"
android:title="@string/empty_trash_action" />
<item android:id="@+id/edit_account"
android:title="@string/account_settings_action" />
<item
android:id="@+id/settings"
android:title="@string/preferences_action"
android:icon="@drawable/ic_menu_preferences">
<menu>
<item android:id="@+id/edit_account"
android:title="@string/account_settings_action" />
<item android:id="@+id/export"
android:title="@string/settings_export_account" />
</menu>
</item>
<item android:id="@+id/advanced"
android:title="@string/advanced">
<menu>

View File

@ -31,8 +31,16 @@
<item android:id="@+id/search"
android:title="@string/search_action" />
-->
<item android:id="@+id/edit_prefs"
<item android:id="@+id/settings"
android:title="@string/preferences_action"
android:icon="@android:drawable/ic_menu_preferences"
/>
android:icon="@android:drawable/ic_menu_preferences">
<menu>
<item android:id="@+id/edit_prefs"
android:title="@string/global_settings_action" />
<item android:id="@+id/export_all"
android:title="@string/settings_export_all" />
<item android:id="@+id/import_settings"
android:title="@string/settings_import" />
</menu>
</item>
</menu>

View File

@ -70,6 +70,10 @@
android:title="@string/global_settings_action"
android:icon="@android:drawable/ic_menu_preferences"
/>
<item android:id="@+id/export"
android:title="@string/settings_export_account" />
<item android:id="@+id/export_all"
android:title="@string/settings_export_all" />
</menu>
</item>
<item

View File

@ -151,6 +151,10 @@
android:title="@string/global_settings_action"
android:icon="@android:drawable/ic_menu_preferences"
/>
<item android:id="@+id/export"
android:title="@string/settings_export_account" />
<item android:id="@+id/export_all"
android:title="@string/settings_export_all" />
</menu>
</item>
<item

View File

@ -79,9 +79,9 @@
<string name="search_results">Search results</string>
<string name="preferences_action">Settings</string>
<string name="open_action">Open</string>
<string name="account_settings_action">Account settings</string>
<string name="folder_settings_action">Folder settings</string>
<string name="global_settings_action">Global settings</string>
<string name="account_settings_action">Edit account settings</string>
<string name="folder_settings_action">Edit folder settings</string>
<string name="global_settings_action">Edit global settings</string>
<string name="remove_account_action">Remove account</string>
<string name="clear_pending_action">Clear pending actions (danger!)</string>
@ -1028,4 +1028,17 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="messagelist_sent_to_me_sigil">»</string>
<string name="messagelist_sent_cc_me_sigil"></string>
<string name="error_unable_to_connect">Unable to connect.</string>
<string name="settings_encryption_password_prompt">Enter settings encryption password:</string>
<string name="settings_export_account">Export account settings</string>
<string name="settings_export_all">Export all settings</string>
<string name="settings_import">Import settings</string>
<string name="settings_exporting">Exporting settings...</string>
<string name="settings_importing">Importing settings...</string>
<string name="settings_export_success">Exported settings to <xliff:g id="filename">%s</xliff:g></string>
<string name="settings_import_success">Imported <xliff:g id="numAccounts">%s</xliff:g> accounts 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>
</resources>

View File

@ -21,7 +21,9 @@ import com.fsck.k9.view.ColorChip;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -478,7 +480,39 @@ public class Account implements BaseAccount
deleteIdentities(preferences.getPreferences(), editor);
editor.commit();
}
public static int findNewAccountNumber(List<Integer> accountNumbers)
{
int newAccountNumber = -1;
Collections.sort(accountNumbers);
for (int accountNumber : accountNumbers)
{
if (accountNumber > newAccountNumber + 1)
{
break;
}
newAccountNumber = accountNumber;
}
newAccountNumber++;
return newAccountNumber;
}
public static List<Integer> getExistingAccountNumbers(Preferences preferences)
{
Account[] accounts = preferences.getAccounts();
List<Integer> accountNumbers = new LinkedList<Integer>();
for (int i = 0; i < accounts.length; i++)
{
accountNumbers.add(accounts[i].getAccountNumber());
}
return accountNumbers;
}
public static int generateAccountNumber(Preferences preferences)
{
List<Integer> accountNumbers = getExistingAccountNumbers(preferences);
return findNewAccountNumber(accountNumbers);
}
public synchronized void save(Preferences preferences)
{
SharedPreferences.Editor editor = preferences.getPreferences().edit();
@ -496,22 +530,7 @@ public class Account implements BaseAccount
*
* I bet there is a much smarter way to do this. Anyone like to suggest it?
*/
Account[] accounts = preferences.getAccounts();
int[] accountNumbers = new int[accounts.length];
for (int i = 0; i < accounts.length; i++)
{
accountNumbers[i] = accounts[i].getAccountNumber();
}
Arrays.sort(accountNumbers);
for (int accountNumber : accountNumbers)
{
if (accountNumber > mAccountNumber + 1)
{
break;
}
mAccountNumber = accountNumber;
}
mAccountNumber++;
mAccountNumber = generateAccountNumber(preferences);
String accountUuids = preferences.getPreferences().getString("accountUuids", "");
accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid;

View File

@ -3,7 +3,12 @@ package com.fsck.k9;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Config;
@ -32,7 +37,8 @@ public class Preferences
private Storage mStorage;
private List<Account> accounts;
private Map<String, Account> accounts = null;
private List<Account> accountsInOrder = null;
private Account newAccount;
private Context mContext;
@ -51,22 +57,44 @@ public class Preferences
private synchronized void loadAccounts()
{
accounts = new HashMap<String, Account>();
refreshAccounts();
}
public synchronized void refreshAccounts()
{
Map<String, Account> newAccountMap = new HashMap<String, Account>();
accountsInOrder = new LinkedList<Account>();
String accountUuids = getPreferences().getString("accountUuids", null);
if ((accountUuids != null) && (accountUuids.length() != 0))
{
String[] uuids = accountUuids.split(",");
accounts = new ArrayList<Account>(uuids.length);
for (String uuid : uuids)
{
accounts.add(new Account(this, uuid));
Account account = accounts.get(uuid);
if (account != null)
{
newAccountMap.put(uuid, account);
accountsInOrder.add(account);
}
else
{
Account newAccount = new Account(this, uuid);
newAccountMap.put(uuid, newAccount);
accountsInOrder.add(newAccount);
}
}
}
else
if ((newAccount != null) && newAccount.getAccountNumber() != -1)
{
accounts = new ArrayList<Account>();
newAccountMap.put(newAccount.getUuid(), newAccount);
accountsInOrder.add(newAccount);
newAccount = null;
}
}
accounts = newAccountMap;
}
/**
* Returns an array of the accounts on the system. If no accounts are
* registered the method returns an empty array.
@ -79,13 +107,7 @@ public class Preferences
loadAccounts();
}
if ((newAccount != null) && newAccount.getAccountNumber() != -1)
{
accounts.add(newAccount);
newAccount = null;
}
return accounts.toArray(EMPTY_ACCOUNT_ARRAY);
return accountsInOrder.toArray(EMPTY_ACCOUNT_ARRAY);
}
/**
@ -95,18 +117,9 @@ public class Preferences
*/
public synchronized Collection<Account> getAvailableAccounts()
{
if (accounts == null)
{
loadAccounts();
}
if ((newAccount != null) && newAccount.getAccountNumber() != -1)
{
accounts.add(newAccount);
newAccount = null;
}
Account[] allAccounts = getAccounts();
Collection<Account> retval = new ArrayList<Account>(accounts.size());
for (Account account : accounts)
for (Account account : allAccounts)
{
if (account.isAvailable(mContext))
{
@ -123,33 +136,24 @@ public class Preferences
{
loadAccounts();
}
for (Account account : accounts)
{
if (account.getUuid().equals(uuid))
{
return account;
}
}
if ((newAccount != null) && newAccount.getUuid().equals(uuid))
{
return newAccount;
}
return null;
Account account = accounts.get(uuid);
return account;
}
public synchronized Account newAccount()
{
newAccount = new Account(K9.app);
accounts.put(newAccount.getUuid(), newAccount);
accountsInOrder.add(newAccount);
return newAccount;
}
public synchronized void deleteAccount(Account account)
{
accounts.remove(account);
accounts.remove(account.getUuid());
accountsInOrder.remove(account);
account.delete(this);
if (newAccount == account)

View File

@ -3,12 +3,15 @@ package com.fsck.k9.activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.util.TypedValue;
@ -29,6 +32,8 @@ import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.view.ColorChip;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -60,6 +65,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
private SearchAccount unreadAccount = null;
private SearchAccount integratedInboxAccount = null;
private FontSizes mFontSizes = K9.getFontSizes();
private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1;
class AccountsHandler extends Handler
{
@ -153,6 +161,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
});
}
}
public void setProgress(boolean progress)
{
mHandler.progress(progress);
}
ActivityListener mListener = new ActivityListener()
{
@ -765,6 +778,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
case R.id.recreate:
onRecreate(realAccount);
break;
case R.id.export:
onExport(realAccount);
break;
}
return true;
}
@ -817,6 +833,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
case R.id.search:
onSearchRequested();
break;
case R.id.export_all:
onExport(null);
break;
case R.id.import_settings:
onImport();
break;
default:
return super.onOptionsItemSelected(item);
}
@ -944,6 +966,98 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
}
}
}
private void onImport()
{
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
Log.i(K9.LOG_TAG, "onActivityResult requestCode = " + requestCode + ", resultCode = " + resultCode + ", data = " + data);
if (resultCode != RESULT_OK)
return;
if (data == null)
{
return;
}
switch (requestCode)
{
case ACTIVITY_REQUEST_PICK_SETTINGS_FILE:
onImport(data.getData());
break;
}
}
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);
String toastText = Accounts.this.getString(R.string.settings_import_failure, fileName, message );
Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1);
toast.show();
}
});
}
public void importSuccess(final int numAccounts)
{
Accounts.this.runOnUiThread(new Runnable()
{
public void run()
{
mHandler.progress(false);
String toastText = Accounts.this.getString(R.string.settings_import_success, numAccounts, fileName );
Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1);
toast.show();
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();
}
}
class AccountsAdapter extends ArrayAdapter<BaseAccount>

View File

@ -0,0 +1,125 @@
package com.fsck.k9.activity;
import java.io.File;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.app.Application;
import android.os.Environment;
import com.fsck.k9.K9;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.preferences.StorageExporter;
import com.fsck.k9.preferences.StorageImporter;
/**
* The class should be used to run long-running processes invoked from the UI that
* do not affect the Stores. There are probably pieces of MessagingController
* that can be moved here.
*
*/
public class AsyncUIProcessor
{
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private Application mApplication;
private static AsyncUIProcessor inst = null;
private AsyncUIProcessor(Application application)
{
mApplication = application;
}
public synchronized static AsyncUIProcessor getInstance(Application application)
{
if (inst == null)
{
inst = new AsyncUIProcessor(application);
}
return inst;
}
public void exportSettings(final String uuid, final String encryptionKey, final ExportListener listener)
{
threadPool.execute(new Runnable()
{
@Override
public void run()
{
try
{
// Do not store with application files. Settings exports should *not* be
// deleted when the application is uninstalled
File dir = new File(Environment.getExternalStorageDirectory() + File.separator
+ mApplication.getPackageName());
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);
}
}
catch (Exception e)
{
listener.failure(e.getLocalizedMessage(), e);
}
}
}
);
}
public void importSettings(final String fileName, final String encryptionKey, final ImportListener listener)
{
threadPool.execute(new Runnable()
{
@Override
public void run()
{
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);
}
}
}
);
}
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);
}
}
}
);
}
}

View File

@ -0,0 +1,67 @@
package com.fsck.k9.activity;
import android.app.Activity;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.R;
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);
String toastText = activity.getString(R.string.settings_export_failure, message);
Toast toast = Toast.makeText(activity.getApplication(), toastText, Toast.LENGTH_LONG);
toast.show();
}
});
}
public void exportSuccess(final String fileName)
{
activity.runOnUiThread(new Runnable()
{
public void run()
{
progressable.setProgress(false);
String toastText = activity.getString(R.string.settings_export_success, fileName );
Toast toast = Toast.makeText(activity.getApplication(), toastText, Toast.LENGTH_LONG);
toast.show();
}
});
}
});
}
public void cancel()
{
}
});
dialog.show();
}
}

View File

@ -0,0 +1,9 @@
package com.fsck.k9.activity;
public interface ExportListener
{
public void exportSuccess(String fileName);
public void failure(String message, Exception e);
}

View File

@ -174,7 +174,11 @@ public class FolderList extends K9ListActivity
});
}
}
public void setProgress(boolean progress)
{
mHandler.progress(progress);
}
/**
* This class is responsible for reloading the list of local messages for a
* given folder, notifying the adapter that the message have been loaded and
@ -628,6 +632,12 @@ public class FolderList extends K9ListActivity
case R.id.compact:
onCompact(mAccount);
case R.id.export:
onExport(mAccount);
return true;
case R.id.export_all:
onExport(null);
return true;
case R.id.display_1st_class:

View File

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

View File

@ -15,11 +15,13 @@ import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.helper.DateFormatter;
public class K9Activity extends Activity
public class K9Activity extends Activity implements Progressable
{
private GestureDetector gestureDetector;
@ -195,6 +197,13 @@ public class K9Activity extends Activity
return false;
}
}
public void setProgress(boolean progress)
{
}
public void onExport(final Account account)
{
ExportHelper.exportSettings(this, this, account);
}
}

View File

@ -5,11 +5,16 @@ import android.util.Log;
import android.view.KeyEvent;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import android.os.Bundle;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.helper.DateFormatter;
public class K9ListActivity extends ListActivity
public class K9ListActivity extends ListActivity implements Progressable
{
@Override
public void onCreate(Bundle icicle)
@ -106,4 +111,14 @@ public class K9ListActivity extends ListActivity
}
return super.onKeyUp(keyCode,event);
}
public void setProgress(boolean progress)
{
}
public void onExport(final Account account)
{
ExportHelper.exportSettings(this, this, account);
}
}

View File

@ -80,7 +80,7 @@ import com.fsck.k9.mail.store.LocalStore.LocalFolder;
*/
public class MessageList
extends K9Activity
implements OnClickListener, AdapterView.OnItemClickListener, AnimationListener
implements OnClickListener, AdapterView.OnItemClickListener, AnimationListener, Progressable
{
/**
@ -596,6 +596,10 @@ public class MessageList
});
}
}
public void setProgress(boolean progress)
{
mHandler.progress(progress);
}
public static void actionHandleFolder(Context context, Account account, String folder)
{
@ -1679,6 +1683,14 @@ public class MessageList
onEditPrefs();
return true;
}
case R.id.export:
onExport(mAccount);
return true;
case R.id.export_all:
onExport(null);
return true;
}
if (mQueryString != null)

View File

@ -0,0 +1,90 @@
package com.fsck.k9.activity;
import com.fsck.k9.R;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class PasswordEntryDialog
{
public interface PasswordEntryListener
{
void passwordChosen(String chosenPassword);
void cancel();
}
PasswordEntryListener listener;
private EditText passwordView;
AlertDialog dialog;
public PasswordEntryDialog(Context context, String headerText, PasswordEntryListener listener )
{
this.listener = listener;
View view = LayoutInflater.from(context).inflate(R.layout.password_entry_dialog, null);
Builder builder = new AlertDialog.Builder(context);
passwordView = (EditText)view.findViewById(R.id.password_text_box);
builder.setView(view);
builder.setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
if (PasswordEntryDialog.this.listener != null)
{
String chosenPassword = passwordView.getText().toString();
PasswordEntryDialog.this.listener.passwordChosen(chosenPassword);
}
}
});
builder.setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
if (PasswordEntryDialog.this.listener != null)
{
PasswordEntryDialog.this.listener.cancel();
}
}
});
dialog = builder.create();
passwordView.addTextChangedListener(new TextWatcher()
{
@Override
public void afterTextChanged(Editable arg0) { }
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) { }
@Override
public void onTextChanged(CharSequence arg0, int arg1, int arg2,
int arg3)
{
Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
String chosenPassword = passwordView.getText().toString();
okButton.setEnabled(chosenPassword.length() > 0);
}
});
dialog.setMessage(headerText);
}
public void show()
{
dialog.show();
Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
okButton.setEnabled(false);
}
}

View File

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

View File

@ -0,0 +1,10 @@
package com.fsck.k9.preferences;
import com.fsck.k9.Preferences;
import android.content.SharedPreferences;
public interface IStorageImporter
{
public abstract int importPreferences(Preferences preferences, SharedPreferences.Editor context, String data, String encryptionKey) throws StorageImportExportException;
}

View File

@ -0,0 +1,90 @@
package com.fsck.k9.preferences;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* package net.sf.andhsli.hotspotlogin;
* Usage:
* <pre>
* String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
* ...
* String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
* </pre>
* @author ferenc.hechler
*/
public class SimpleCrypto {
public static String encrypt(String seed, String cleartext, Base64 base64) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] result = encrypt(rawKey, cleartext.getBytes());
return new String(base64.encode(result));
}
public static String decrypt(String seed, String encrypted, Base64 base64) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] enc = base64.decode(encrypted.getBytes());
byte[] result = decrypt(rawKey, enc);
return new String(result);
}
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(seed);
kgen.init(128, sr); // 192 and 256 bits may not be available
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}
private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
//
// public static byte[] toByte(String hexString) {
// int len = hexString.length()/2;
// byte[] result = new byte[len];
// for (int i = 0; i < len; i++)
// result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
// return result;
// }
//
// public static String toHex(byte[] buf) {
// if (buf == null)
// return "";
// StringBuffer result = new StringBuffer(2*buf.length);
// for (int i = 0; i < buf.length; i++) {
// appendHex(result, buf[i]);
// }
// return result.toString();
// }
// private final static String HEX = "0123456789ABCDEF";
// private static void appendHex(StringBuffer sb, byte b) {
// sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
// }
//
}

View File

@ -0,0 +1,81 @@
package com.fsck.k9.preferences;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
public class StorageExporter
{
//public static String VALIDITY = "K-9MailExport"; // Does outputting a fixed string in a known location make the encrypted data easier to break?
public static void exportPreferences(Context context, String uuid, String fileName, String encryptionKey) throws StorageImportExportException
{
try
{
Base64 base64 = new Base64();
File outFile = new File(fileName);
PrintWriter pf = new PrintWriter(outFile);
long keysEvaluated = 0;
long keysExported = 0;
pf.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
// String testval = SimpleCrypto.encrypt(encryptionKey, VALIDITY);
pf.print("<k9settings version=\"1\"");
//pf.print(" validity=\"" + testval + "\"");
pf.println(">");
Log.i(K9.LOG_TAG, "Exporting preferences for account " + uuid + " to file " + fileName);
SharedPreferences storage = Preferences.getPreferences(context).getPreferences();
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;
}
}
String keyEnc = SimpleCrypto.encrypt(encryptionKey, key, base64);
String valueEnc = SimpleCrypto.encrypt(encryptionKey, value, base64);
String output = keyEnc + ":" + valueEnc;
//Log.i(K9.LOG_TAG, "For key " + key + ", output is " + output);
pf.println(output);
keysExported++;
}
pf.println("</k9settings>");
pf.close();
Log.i(K9.LOG_TAG, "Exported " + keysExported + " settings of " + keysEvaluated
+ " total for preferences for account " + uuid + " to file " + fileName + " which is size " + outFile.length());
}
catch (IOException ie)
{
throw new StorageImportExportException("Unable to export settings", ie);
}
catch (Exception e)
{
throw new StorageImportExportException("Unable to encrypt settings", e);
}
}
}

View File

@ -0,0 +1,26 @@
package com.fsck.k9.preferences;
public class StorageImportExportException extends Exception
{
public StorageImportExportException()
{
super();
}
public StorageImportExportException(String detailMessage, Throwable throwable)
{
super(detailMessage, throwable);
}
public StorageImportExportException(String detailMessage)
{
super(detailMessage);
}
public StorageImportExportException(Throwable throwable)
{
super(throwable);
}
}

View File

@ -0,0 +1,167 @@
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;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
public class StorageImporter
{
public static int importPreferences(Context context, String fileName, String encryptionKey) throws StorageImportExportException
{
try
{
InputStream is = new FileInputStream(fileName);
return importPreferences(context, is, encryptionKey);
}
catch (FileNotFoundException fnfe)
{
throw new StorageImportExportException("Failure reading settings file " + fileName, fnfe);
}
}
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();
StorageImporterHandler handler = new StorageImporterHandler();
xr.setContentHandler(handler);
xr.parse(new InputSource(is));
is.close();
Element 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)");
}
int numAccounts = 0;
if (storageImporter != null)
{
String data = dataset.data.toString();
numAccounts = storageImporter.importPreferences(preferences, editor, data, encryptionKey);
}
editor.commit();
Preferences.getPreferences(context).refreshAccounts();
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 class Element
{
String name;
Map<String, String> attributes = new HashMap<String, String>();
Map<String, Element> subElements = new HashMap<String, Element>();
StringBuilder data = new StringBuilder();
}
private static class StorageImporterHandler extends DefaultHandler
{
private Element rootElement = new Element();
private Stack<Element> mOpenTags = new Stack<Element>();
public Element getRootElement()
{
return this.rootElement;
}
@Override
public void startDocument() throws SAXException
{
}
@Override
public void endDocument() throws SAXException
{
/* Do nothing */
}
@Override
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();
element.name = localName;
mOpenTags.push(element);
for (int i = 0; i < attributes.getLength(); i++)
{
String key = attributes.getLocalName(i);
String value = attributes.getValue(i);
Log.i(K9.LOG_TAG, "Got attribute " + key + " = " + value);
element.attributes.put(key, value);
}
}
@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();
if (superElement != null)
{
superElement.subElements.put(element.name, element);
}
else
{
rootElement = element;
}
}
@Override
public void characters(char ch[], int start, int length)
{
String value = new String(ch, start, length);
mOpenTags.peek().data.append(value);
}
}
}

View File

@ -0,0 +1,103 @@
package com.fsck.k9.preferences;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.codec.binary.Base64;
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 StorageImporterVersion1 implements IStorageImporter
{
public int importPreferences(Preferences preferences, SharedPreferences.Editor editor, String data, String encryptionKey) throws StorageImportExportException
{
try
{
Base64 base64 = new Base64();
List<Integer> accountNumbers = Account.getExistingAccountNumbers(preferences);
Log.i(K9.LOG_TAG, "Existing accountNumbers = " + accountNumbers);
Map<String, String> uuidMapping = new HashMap<String, String>();
String accountUuids = preferences.getPreferences().getString("accountUuids", null);
StringReader sr = new StringReader(data);
BufferedReader br = new BufferedReader(sr);
String line = null;
int settingsImported = 0;
int numAccounts = 0;
do
{
line = br.readLine();
if (line != null)
{
//Log.i(K9.LOG_TAG, "Got line " + line);
String[] comps = line.split(":");
if (comps.length > 1)
{
String keyEnc = comps[0];
String valueEnc = comps[1];
String key = SimpleCrypto.decrypt(encryptionKey, keyEnc, base64);
String value = SimpleCrypto.decrypt(encryptionKey, valueEnc, base64);
String[] keyParts = key.split("\\.");
if (keyParts.length > 1)
{
String oldUuid = keyParts[0];
String newUuid = uuidMapping.get(oldUuid);
if (newUuid == null)
{
newUuid = UUID.randomUUID().toString();
uuidMapping.put(oldUuid, newUuid);
Log.i(K9.LOG_TAG, "Mapping oldUuid " + oldUuid + " to newUuid " + newUuid);
}
keyParts[0] = newUuid;
if ("accountNumber".equals(keyParts[1]))
{
int accountNumber = Account.findNewAccountNumber(accountNumbers);
accountNumbers.add(accountNumber);
value = Integer.toString(accountNumber);
accountUuids += (accountUuids.length() != 0 ? "," : "") + newUuid;
numAccounts++;
}
StringBuilder builder = new StringBuilder();
for (String part : keyParts)
{
if (builder.length() > 0)
{
builder.append(".");
}
builder.append(part);
}
key = builder.toString();
}
//Log.i(K9.LOG_TAG, "Setting " + key + " = " + value);
settingsImported++;
editor.putString(key, value);
}
}
} while (line != null);
editor.putString("accountUuids", accountUuids);
Log.i(K9.LOG_TAG, "Imported " + settingsImported + " settings and " + numAccounts + " accounts");
return numAccounts;
}
catch (IOException ie)
{
throw new StorageImportExportException("Unable to import settings", ie);
}
catch (Exception e)
{
throw new StorageImportExportException("Unable to decrypt settings", e);
}
}
}