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.
This commit is contained in:
Daniel Applebaum 2009-04-11 14:33:54 +00:00
parent 06a90571cd
commit 932adf5ed2
5 changed files with 516 additions and 58 deletions

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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<String, String> changes = new HashMap<String, String>();
private ArrayList<String> removals = new ArrayList<String>();
private boolean removeAll = false;
Map<String, String> snapshot = new HashMap<String, String>();
protected Editor(Storage storage)
{
this.storage = storage;
snapshot.putAll(storage.getAll());
}
public void copy(android.content.SharedPreferences input)
{
Map<String, ?> oldVals = input.getAll();
for (Entry<String, ?> 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<String, String> 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;
}
}

View file

@ -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<Context, Storage> storages =
new ConcurrentHashMap<Context, Storage>();
private volatile ConcurrentHashMap<String, String> storage = new ConcurrentHashMap<String, String>();
private CopyOnWriteArrayList<OnSharedPreferenceChangeListener> listeners =
new CopyOnWriteArrayList<OnSharedPreferenceChangeListener>();
private int DB_VERSION = 1; // CHANGING THIS WILL DESTROY ALL USER PREFERENCES!
private String DB_NAME = "preferences_storage";
private ThreadLocal<ConcurrentHashMap<String, String>> workingStorage
= new ThreadLocal<ConcurrentHashMap<String, String>>();
private ThreadLocal<SQLiteDatabase> workingDB =
new ThreadLocal<SQLiteDatabase>();
private ThreadLocal<ArrayList<String>> workingChangedKeys = new ThreadLocal<ArrayList<String>>();
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<String> 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<String, String> newStorage = new ConcurrentHashMap<String, String>();
newStorage.putAll(storage);
workingStorage.set(newStorage);
SQLiteDatabase mDb = openDB();
workingDB.set(mDb);
ArrayList<String> changedKeys = new ArrayList<String>();
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<String, String> 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);
}
}