From 932adf5ed2ac2903cdba9e6602ab4de368d67a76 Mon Sep 17 00:00:00 2001 From: Daniel Applebaum Date: Sat, 11 Apr 2009 14:33:54 +0000 Subject: [PATCH] Issue 143 Merged from branch issue143 @ revision 426: Complete replacement for SharedPreferences. Uses SQLite database stored in application's databases folder. Will load from legacy preferences if DB-backed preferences are empty. Editor conforms to atomic commit contract. --- src/com/android/email/Account.java | 69 ++--- src/com/android/email/Preferences.java | 45 +-- .../android/email/mail/store/LocalStore.java | 8 +- src/com/android/email/preferences/Editor.java | 161 ++++++++++ .../android/email/preferences/Storage.java | 291 ++++++++++++++++++ 5 files changed, 516 insertions(+), 58 deletions(-) create mode 100644 src/com/android/email/preferences/Editor.java create mode 100644 src/com/android/email/preferences/Storage.java diff --git a/src/com/android/email/Account.java b/src/com/android/email/Account.java index 46e542066..41bf07589 100644 --- a/src/com/android/email/Account.java +++ b/src/com/android/email/Account.java @@ -101,35 +101,35 @@ public class Account implements Serializable { * Refresh the account from the stored settings. */ public void refresh(Preferences preferences) { - mStoreUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid + mStoreUri = Utility.base64Decode(preferences.getPreferences().getString(mUuid + ".storeUri", null)); - mLocalStoreUri = preferences.mSharedPreferences.getString(mUuid + ".localStoreUri", null); - mTransportUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid + mLocalStoreUri = preferences.getPreferences().getString(mUuid + ".localStoreUri", null); + mTransportUri = Utility.base64Decode(preferences.getPreferences().getString(mUuid + ".transportUri", null)); - mDescription = preferences.mSharedPreferences.getString(mUuid + ".description", null); - mAlwaysBcc = preferences.mSharedPreferences.getString(mUuid + ".alwaysBcc", mAlwaysBcc); - mName = preferences.mSharedPreferences.getString(mUuid + ".name", mName); - mEmail = preferences.mSharedPreferences.getString(mUuid + ".email", mEmail); - mSignature = preferences.mSharedPreferences.getString(mUuid + ".signature", mSignature); - mAutomaticCheckIntervalMinutes = preferences.mSharedPreferences.getInt(mUuid + mDescription = preferences.getPreferences().getString(mUuid + ".description", null); + mAlwaysBcc = preferences.getPreferences().getString(mUuid + ".alwaysBcc", mAlwaysBcc); + mName = preferences.getPreferences().getString(mUuid + ".name", mName); + mEmail = preferences.getPreferences().getString(mUuid + ".email", mEmail); + mSignature = preferences.getPreferences().getString(mUuid + ".signature", mSignature); + mAutomaticCheckIntervalMinutes = preferences.getPreferences().getInt(mUuid + ".automaticCheckIntervalMinutes", -1); - mDisplayCount = preferences.mSharedPreferences.getInt(mUuid + ".displayCount", -1); - mLastAutomaticCheckTime = preferences.mSharedPreferences.getLong(mUuid + mDisplayCount = preferences.getPreferences().getInt(mUuid + ".displayCount", -1); + mLastAutomaticCheckTime = preferences.getPreferences().getLong(mUuid + ".lastAutomaticCheckTime", 0); - mNotifyNewMail = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyNewMail", + mNotifyNewMail = preferences.getPreferences().getBoolean(mUuid + ".notifyNewMail", false); - mNotifySync = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyMailCheck", + mNotifySync = preferences.getPreferences().getBoolean(mUuid + ".notifyMailCheck", false); - mDeletePolicy = preferences.mSharedPreferences.getInt(mUuid + ".deletePolicy", 0); - mDraftsFolderName = preferences.mSharedPreferences.getString(mUuid + ".draftsFolderName", + mDeletePolicy = preferences.getPreferences().getInt(mUuid + ".deletePolicy", 0); + mDraftsFolderName = preferences.getPreferences().getString(mUuid + ".draftsFolderName", "Drafts"); - mSentFolderName = preferences.mSharedPreferences.getString(mUuid + ".sentFolderName", + mSentFolderName = preferences.getPreferences().getString(mUuid + ".sentFolderName", "Sent"); - mTrashFolderName = preferences.mSharedPreferences.getString(mUuid + ".trashFolderName", + mTrashFolderName = preferences.getPreferences().getString(mUuid + ".trashFolderName", "Trash"); - mOutboxFolderName = preferences.mSharedPreferences.getString(mUuid + ".outboxFolderName", + mOutboxFolderName = preferences.getPreferences().getString(mUuid + ".outboxFolderName", "Outbox"); - + // Between r418 and r431 (version 0.103), folder names were set empty if the Incoming settings were // opened for non-IMAP accounts. 0.103 was never a market release, so perhaps this code // should be deleted sometime soon @@ -151,14 +151,15 @@ public class Account implements Serializable { } // End of 0.103 repair - mAutoExpandFolderName = preferences.mSharedPreferences.getString(mUuid + ".autoExpandFolderName", + mAutoExpandFolderName = preferences.getPreferences().getString(mUuid + ".autoExpandFolderName", "Inbox"); - mAccountNumber = preferences.mSharedPreferences.getInt(mUuid + ".accountNumber", 0); - mVibrate = preferences.mSharedPreferences.getBoolean(mUuid + ".vibrate", false); + + mAccountNumber = preferences.getPreferences().getInt(mUuid + ".accountNumber", 0); + mVibrate = preferences.getPreferences().getBoolean(mUuid + ".vibrate", false); try { - mHideMessageViewButtons = HideButtons.valueOf(preferences.mSharedPreferences.getString(mUuid + ".hideButtonsEnum", + mHideMessageViewButtons = HideButtons.valueOf(preferences.getPreferences().getString(mUuid + ".hideButtonsEnum", HideButtons.NEVER.name())); } catch (Exception e) @@ -166,11 +167,11 @@ public class Account implements Serializable { mHideMessageViewButtons = HideButtons.NEVER; } - mRingtoneUri = preferences.mSharedPreferences.getString(mUuid + ".ringtone", + mRingtoneUri = preferences.getPreferences().getString(mUuid + ".ringtone", "content://settings/system/notification_sound"); try { - mFolderDisplayMode = FolderMode.valueOf(preferences.mSharedPreferences.getString(mUuid + ".folderDisplayMode", + mFolderDisplayMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderDisplayMode", FolderMode.NOT_SECOND_CLASS.name())); } catch (Exception e) @@ -180,7 +181,7 @@ public class Account implements Serializable { try { - mFolderSyncMode = FolderMode.valueOf(preferences.mSharedPreferences.getString(mUuid + ".folderSyncMode", + mFolderSyncMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderSyncMode", FolderMode.FIRST_CLASS.name())); } catch (Exception e) @@ -190,7 +191,7 @@ public class Account implements Serializable { try { - mFolderTargetMode = FolderMode.valueOf(preferences.mSharedPreferences.getString(mUuid + ".folderTargetMode", + mFolderTargetMode = FolderMode.valueOf(preferences.getPreferences().getString(mUuid + ".folderTargetMode", FolderMode.NOT_SECOND_CLASS.name())); } catch (Exception e) @@ -278,7 +279,7 @@ public class Account implements Serializable { } public void delete(Preferences preferences) { - String[] uuids = preferences.mSharedPreferences.getString("accountUuids", "").split(","); + String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(","); StringBuffer sb = new StringBuffer(); for (int i = 0, length = uuids.length; i < length; i++) { if (!uuids[i].equals(mUuid)) { @@ -289,7 +290,7 @@ public class Account implements Serializable { } } String accountUuids = sb.toString(); - SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + SharedPreferences.Editor editor = preferences.getPreferences().edit(); editor.putString("accountUuids", accountUuids); editor.remove(mUuid + ".storeUri"); @@ -320,9 +321,9 @@ public class Account implements Serializable { } public void save(Preferences preferences) { - SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + SharedPreferences.Editor editor = preferences.getPreferences().edit(); - if (!preferences.mSharedPreferences.getString("accountUuids", "").contains(mUuid)) { + if (!preferences.getPreferences().getString("accountUuids", "").contains(mUuid)) { /* * When the account is first created we assign it a unique account number. The * account number will be unique to that account for the lifetime of the account. @@ -348,15 +349,11 @@ public class Account implements Serializable { } mAccountNumber++; - String accountUuids = preferences.mSharedPreferences.getString("accountUuids", ""); + String accountUuids = preferences.getPreferences().getString("accountUuids", ""); accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid; - // SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); editor.putString("accountUuids", accountUuids); - // editor.commit(); } - // SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); - editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri)); editor.putString(mUuid + ".localStoreUri", mLocalStoreUri); editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri)); diff --git a/src/com/android/email/Preferences.java b/src/com/android/email/Preferences.java index f57839879..68a611093 100644 --- a/src/com/android/email/Preferences.java +++ b/src/com/android/email/Preferences.java @@ -3,6 +3,9 @@ package com.android.email; import java.util.Arrays; +import com.android.email.preferences.Editor; +import com.android.email.preferences.Storage; + import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; @@ -12,11 +15,19 @@ import android.util.Log; public class Preferences { private static Preferences preferences; - public SharedPreferences mSharedPreferences; + private Storage mStorage; private Preferences(Context context) { - mSharedPreferences = context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE); + mStorage = Storage.getStorage(context); + if (mStorage.size() == 0) + { + Log.i(Email.LOG_TAG, "Preferences storage is zero-size, importing from Android-style preferences"); + Editor editor = mStorage.edit(); + editor.copy(context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE)); + editor.commit(); + } } + /** * TODO need to think about what happens if this gets GCed along with the @@ -40,7 +51,7 @@ public class Preferences { * @return */ public Account[] getAccounts() { - String accountUuids = mSharedPreferences.getString("accountUuids", null); + String accountUuids = getPreferences().getString("accountUuids", null); if (accountUuids == null || accountUuids.length() == 0) { return new Account[] {}; } @@ -64,7 +75,7 @@ public class Preferences { * @return */ public Account getDefaultAccount() { - String defaultAccountUuid = mSharedPreferences.getString("defaultAccountUuid", null); + String defaultAccountUuid = getPreferences().getString("defaultAccountUuid", null); Account defaultAccount = null; Account[] accounts = getAccounts(); if (defaultAccountUuid != null) { @@ -87,37 +98,35 @@ public class Preferences { } public void setDefaultAccount(Account account) { - mSharedPreferences.edit().putString("defaultAccountUuid", account.getUuid()).commit(); + getPreferences().edit().putString("defaultAccountUuid", account.getUuid()).commit(); } public void setEnableDebugLogging(boolean value) { - mSharedPreferences.edit().putBoolean("enableDebugLogging", value).commit(); + getPreferences().edit().putBoolean("enableDebugLogging", value).commit(); } public boolean geteEnableDebugLogging() { - return mSharedPreferences.getBoolean("enableDebugLogging", false); + return getPreferences().getBoolean("enableDebugLogging", false); } public void setEnableSensitiveLogging(boolean value) { - mSharedPreferences.edit().putBoolean("enableSensitiveLogging", value).commit(); + getPreferences().edit().putBoolean("enableSensitiveLogging", value).commit(); } public boolean getEnableSensitiveLogging() { - return mSharedPreferences.getBoolean("enableSensitiveLogging", false); - } - - public void save() { - } - - public void clear() { - mSharedPreferences.edit().clear().commit(); + return getPreferences().getBoolean("enableSensitiveLogging", false); } public void dump() { if (Config.LOGV) { - for (String key : mSharedPreferences.getAll().keySet()) { - Log.v(Email.LOG_TAG, key + " = " + mSharedPreferences.getAll().get(key)); + for (String key : getPreferences().getAll().keySet()) { + Log.v(Email.LOG_TAG, key + " = " + getPreferences().getAll().get(key)); } } } + + public SharedPreferences getPreferences() + { + return mStorage; + } } diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java index a5decf6cf..6e7bbc00b 100644 --- a/src/com/android/email/mail/store/LocalStore.java +++ b/src/com/android/email/mail/store/LocalStore.java @@ -701,7 +701,7 @@ public class LocalStore extends Store implements Serializable { public void delete(Preferences preferences) throws MessagingException { String id = getPrefId(); - SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + SharedPreferences.Editor editor = preferences.getPreferences().edit(); editor.remove(id + ".displayMode"); editor.remove(id + ".syncMode"); @@ -712,7 +712,7 @@ public class LocalStore extends Store implements Serializable { public void save(Preferences preferences) throws MessagingException { String id = getPrefId(); - SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + SharedPreferences.Editor editor = preferences.getPreferences().edit(); // there can be a lot of folders. For the defaults, let's not save prefs, saving space, except for INBOX if (displayClass == FolderClass.NONE && !Email.INBOX.equals(getName())) { @@ -740,7 +740,7 @@ public class LocalStore extends Store implements Serializable { try { - displayClass = FolderClass.valueOf(preferences.mSharedPreferences.getString(id + ".displayMode", + displayClass = FolderClass.valueOf(preferences.getPreferences().getString(id + ".displayMode", FolderClass.NONE.name())); } catch (Exception e) @@ -759,7 +759,7 @@ public class LocalStore extends Store implements Serializable { try { - syncClass = FolderClass.valueOf(preferences.mSharedPreferences.getString(id + ".syncMode", + syncClass = FolderClass.valueOf(preferences.getPreferences().getString(id + ".syncMode", defSyncClass.name())); } catch (Exception e) diff --git a/src/com/android/email/preferences/Editor.java b/src/com/android/email/preferences/Editor.java new file mode 100644 index 000000000..f9afed36d --- /dev/null +++ b/src/com/android/email/preferences/Editor.java @@ -0,0 +1,161 @@ +package com.android.email.preferences; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.android.email.Email; + +import android.util.Log; + +public class Editor implements android.content.SharedPreferences.Editor +{ + private Storage storage; + private HashMap changes = new HashMap(); + private ArrayList removals = new ArrayList(); + private boolean removeAll = false; + + Map snapshot = new HashMap(); + + + protected Editor(Storage storage) + { + this.storage = storage; + snapshot.putAll(storage.getAll()); + } + + public void copy(android.content.SharedPreferences input) + { + Map oldVals = input.getAll(); + for (Entry entry : oldVals.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + if (key != null && value != null) + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Copying key '" + key + "', value '" + value + "'"); + } + changes.put(key, "" + value); + } + else + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Skipping copying key '" + key + "', value '" + value + "'"); + } + } + } + } + + @Override + public android.content.SharedPreferences.Editor clear() + { + removeAll = true; + return this; + } + + /* This method is poorly defined. It should throw an Exception on failure */ + @Override + public boolean commit() + { + try + { + commitChanges(); + return true; + } + catch (Exception e) + { + Log.e(Email.LOG_TAG, "Failed to save preferences", e); + return false; + } + } + + public void commitChanges() throws Exception + { + long startTime = System.currentTimeMillis(); + Log.i(Email.LOG_TAG, "Committing preference changes"); + Runnable committer = new Runnable() { + public void run() + { + if (removeAll) + { + storage.removeAll(); + } + for (String removeKey : removals) + { + storage.remove(removeKey); + } + for (Entry entry : changes.entrySet()) + { + String key = entry.getKey(); + String newValue = entry.getValue(); + String oldValue = snapshot.get(key); + if (removeAll || removals.contains(key) || newValue.equals(oldValue) != true) + { + storage.put(key, newValue); + } + } + } + }; + storage.doInTransaction(committer); + long endTime = System.currentTimeMillis(); + Log.i(Email.LOG_TAG, "Preferences commit took " + (endTime - startTime) + "ms"); + + } + + @Override + public android.content.SharedPreferences.Editor putBoolean(String key, + boolean value) + { + changes.put(key, "" + value); + return this; + } + + @Override + public android.content.SharedPreferences.Editor putFloat(String key, + float value) + { + changes.put(key, "" + value); + return this; + } + + @Override + public android.content.SharedPreferences.Editor putInt(String key, int value) + { + changes.put(key, "" + value); + return this; + } + + @Override + public android.content.SharedPreferences.Editor putLong(String key, long value) + { + changes.put(key, "" + value); + return this; + } + + @Override + public android.content.SharedPreferences.Editor putString(String key, + String value) + { + if (value == null) + { + remove(key); + } + else + { + changes.put(key, value); + } + return this; + } + + @Override + public android.content.SharedPreferences.Editor remove(String key) + { + removals.add(key); + return this; + } + +} diff --git a/src/com/android/email/preferences/Storage.java b/src/com/android/email/preferences/Storage.java new file mode 100644 index 000000000..1d9293c7c --- /dev/null +++ b/src/com/android/email/preferences/Storage.java @@ -0,0 +1,291 @@ +package com.android.email.preferences; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.android.email.Email; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +public class Storage implements SharedPreferences +{ + private static ConcurrentHashMap storages = + new ConcurrentHashMap(); + + private volatile ConcurrentHashMap storage = new ConcurrentHashMap(); + + private CopyOnWriteArrayList listeners = + new CopyOnWriteArrayList(); + + private int DB_VERSION = 1; // CHANGING THIS WILL DESTROY ALL USER PREFERENCES! + private String DB_NAME = "preferences_storage"; + + private ThreadLocal> workingStorage + = new ThreadLocal>(); + private ThreadLocal workingDB = + new ThreadLocal(); + private ThreadLocal> workingChangedKeys = new ThreadLocal>(); + + + private Context context = null; + + private SQLiteDatabase openDB() + { + SQLiteDatabase mDb = context.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); + if (mDb.getVersion() != DB_VERSION) + { + Log.i(Email.LOG_TAG, "Creating Storage database"); + mDb.execSQL("DROP TABLE IF EXISTS preferences_storage"); + mDb.execSQL("CREATE TABLE preferences_storage " + + "(primkey TEXT PRIMARY KEY ON CONFLICT REPLACE, value TEXT)"); + mDb.setVersion(DB_VERSION); + } + return mDb; + } + + + public static Storage getStorage(Context context) + { + Storage tmpStorage = storages.get(context); + if (tmpStorage != null) + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Returning already existing Storage"); + } + return tmpStorage; + } + else + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Creating provisional storage"); + } + tmpStorage = new Storage(context); + Storage oldStorage = storages.putIfAbsent(context, tmpStorage); + if (oldStorage != null) + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Another thread beat us to creating the Storage, returning that one"); + } + return oldStorage; + } + else + { + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Returning the Storage we created"); + } + return tmpStorage; + } + } + } + + private void loadValues() + { + long startTime = System.currentTimeMillis(); + Log.i(Email.LOG_TAG, "Loading preferences from DB into Storage"); + Cursor cursor = null; + try { + SQLiteDatabase mDb = openDB(); + + cursor = mDb.rawQuery("SELECT primkey, value FROM preferences_storage", null); + while (cursor.moveToNext()) { + String key = cursor.getString(0); + String value = cursor.getString(1); + if (Email.DEBUG) + { + Log.d(Email.LOG_TAG, "Loading key '" + key + "', value = '" + value + "'"); + } + storage.put(key, value); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + long endTime = System.currentTimeMillis(); + Log.i(Email.LOG_TAG, "Preferences load took " + (endTime - startTime) + "ms"); + } + } + + private Storage(Context context) + { + this.context = context; + loadValues(); + } + + private void keyChange(String key) + { + ArrayList changedKeys = workingChangedKeys.get(); + if (changedKeys.contains(key) == false) + { + changedKeys.add(key); + } + } + + protected void put(String key, String value) + { + ContentValues cv = new ContentValues(); + cv.put("primkey", key); + cv.put("value", value); + workingDB.get().insert("preferences_storage", "primkey", cv); + workingStorage.get().put(key, value); + + keyChange(key); + } + + protected void remove(String key) + { + workingDB.get().delete("preferences_storage", "primkey = ?", new String[] { key }); + workingStorage.get().remove(key); + + keyChange(key); + } + + protected void removeAll() + { + for (String key : workingStorage.get().keySet()) + { + keyChange(key); + } + workingDB.get().execSQL("DELETE FROM preferences_storage"); + workingStorage.get().clear(); + } + + protected void doInTransaction(Runnable dbWork) + { + ConcurrentHashMap newStorage = new ConcurrentHashMap(); + newStorage.putAll(storage); + workingStorage.set(newStorage); + + SQLiteDatabase mDb = openDB(); + workingDB.set(mDb); + + ArrayList changedKeys = new ArrayList(); + workingChangedKeys.set(changedKeys); + + mDb.beginTransaction(); + try + { + dbWork.run(); + mDb.setTransactionSuccessful(); + storage = newStorage; + for (String changedKey : changedKeys) + { + for (OnSharedPreferenceChangeListener listener : listeners) + { + listener.onSharedPreferenceChanged(this, changedKey); + } + } + } + finally + { + workingDB.remove(); + workingStorage.remove(); + workingChangedKeys.remove(); + mDb.endTransaction(); + } + } + + public long size() + { + return storage.size(); + } + + @Override + public boolean contains(String key) + { + return storage.contains(key); + } + + @Override + public com.android.email.preferences.Editor edit() + { + return new com.android.email.preferences.Editor(this); + } + + @Override + public Map getAll() + { + return storage; + } + + @Override + public boolean getBoolean(String key, boolean defValue) + { + String val = storage.get(key); + if (val == null) + { + return defValue; + } + return Boolean.parseBoolean(val); + } + + @Override + public float getFloat(String key, float defValue) + { + String val = storage.get(key); + if (val == null) + { + return defValue; + } + return Float.parseFloat(val); + } + + @Override + public int getInt(String key, int defValue) + { + String val = storage.get(key); + if (val == null) + { + return defValue; + } + return Integer.parseInt(val); + } + + @Override + public long getLong(String key, long defValue) + { + String val = storage.get(key); + if (val == null) + { + return defValue; + } + return Long.parseLong(val); + } + + @Override + public String getString(String key, String defValue) + { + String val = storage.get(key); + if (val == null) + { + return defValue; + } + return val; + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) + { + listeners.addIfAbsent(listener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) + { + listeners.remove(listener); + } + +}