extract persistence logic from Storage into StoragePersister
This commit is contained in:
parent
d1c4701256
commit
f7faccf5d9
20 changed files with 339 additions and 316 deletions
|
@ -192,7 +192,7 @@ class AccountPreferenceSerializer(
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
fun save(storage: Storage, editor: StorageEditor, account: Account) {
|
||||
fun save(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
if (!storage.getString("accountUuids", "").contains(account.uuid)) {
|
||||
|
@ -294,13 +294,11 @@ class AccountPreferenceSerializer(
|
|||
}
|
||||
|
||||
saveIdentities(account, storage, editor)
|
||||
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
fun delete(storage: Storage, account: Account) {
|
||||
fun delete(editor: StorageEditor, storage: Storage, account: Account) {
|
||||
val accountUuid = account.uuid
|
||||
|
||||
// Get the list of account UUIDs
|
||||
|
@ -314,8 +312,6 @@ class AccountPreferenceSerializer(
|
|||
}
|
||||
}
|
||||
|
||||
val editor = storage.edit()
|
||||
|
||||
// Only change the 'accountUuids' value if this account's UUID was listed before
|
||||
if (newUuids.size < uuids.size) {
|
||||
val accountUuids = Utility.combine(newUuids.toTypedArray(), ',')
|
||||
|
@ -407,7 +403,6 @@ class AccountPreferenceSerializer(
|
|||
}
|
||||
deleteIdentities(account, storage, editor)
|
||||
// TODO: Remove preference settings that may exist for individual folders in the account.
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -450,9 +445,8 @@ class AccountPreferenceSerializer(
|
|||
} while (gotOne)
|
||||
}
|
||||
|
||||
fun move(account: Account, storage: Storage, moveUp: Boolean) {
|
||||
fun move(editor: StorageEditor, account: Account, storage: Storage, moveUp: Boolean) {
|
||||
val uuids = storage.getString("accountUuids", "").split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val editor = storage.edit()
|
||||
val newUuids = arrayOfNulls<String>(uuids.size)
|
||||
if (moveUp) {
|
||||
for (i in uuids.indices) {
|
||||
|
@ -475,7 +469,6 @@ class AccountPreferenceSerializer(
|
|||
}
|
||||
val accountUuids = Utility.combine(newUuids, ',')
|
||||
editor.putString("accountUuids", accountUuids)
|
||||
editor.commit()
|
||||
}
|
||||
|
||||
private fun <T : Enum<T>> getEnumStringPref(storage: Storage, key: String, defaultEnum: T): T {
|
||||
|
|
|
@ -383,7 +383,7 @@ public class K9 {
|
|||
preferences.saveAccount(account);
|
||||
}
|
||||
|
||||
storage.edit()
|
||||
preferences.createStorageEditor()
|
||||
.remove("openPgpProvider")
|
||||
.remove("openPgpSupportSignOnly")
|
||||
.commit();
|
||||
|
@ -1075,7 +1075,7 @@ public class K9 {
|
|||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
Preferences prefs = DI.get(Preferences.class);
|
||||
StorageEditor editor = prefs.getStorage().edit();
|
||||
StorageEditor editor = prefs.createStorageEditor();
|
||||
save(editor);
|
||||
editor.commit();
|
||||
|
||||
|
|
|
@ -10,12 +10,14 @@ import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
|||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
import com.fsck.k9.mailstore.StorageManager
|
||||
import com.fsck.k9.power.TracingPowerManager
|
||||
import com.fsck.k9.preferences.StoragePersister
|
||||
import org.koin.dsl.module.applicationContext
|
||||
|
||||
val mainModule = applicationContext {
|
||||
bean { Preferences.getPreferences(get()) }
|
||||
bean { get<Context>().resources }
|
||||
bean { StorageManager.getInstance(get()) }
|
||||
bean { StoragePersister(get()) }
|
||||
bean { LocalStoreProvider() }
|
||||
bean { TracingPowerManager.getPowerManager(get()) as PowerManager }
|
||||
bean { Contacts.getInstance(get()) }
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.fsck.k9.mailstore.LocalStore;
|
|||
import com.fsck.k9.mailstore.LocalStoreProvider;
|
||||
import com.fsck.k9.preferences.Storage;
|
||||
import com.fsck.k9.preferences.StorageEditor;
|
||||
import com.fsck.k9.preferences.StoragePersister;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
|
@ -30,13 +31,15 @@ public class Preferences {
|
|||
private AccountPreferenceSerializer accountPreferenceSerializer;
|
||||
|
||||
public static synchronized Preferences getPreferences(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
CoreResourceProvider resourceProvider = DI.get(CoreResourceProvider.class);
|
||||
LocalKeyStoreManager localKeyStoreManager = DI.get(LocalKeyStoreManager.class);
|
||||
AccountPreferenceSerializer accountPreferenceSerializer = DI.get(AccountPreferenceSerializer.class);
|
||||
LocalStoreProvider localStoreProvider = DI.get(LocalStoreProvider.class);
|
||||
if (preferences == null) {
|
||||
preferences = new Preferences(appContext, resourceProvider, localStoreProvider, localKeyStoreManager, accountPreferenceSerializer);
|
||||
Context appContext = context.getApplicationContext();
|
||||
CoreResourceProvider resourceProvider = DI.get(CoreResourceProvider.class);
|
||||
LocalKeyStoreManager localKeyStoreManager = DI.get(LocalKeyStoreManager.class);
|
||||
AccountPreferenceSerializer accountPreferenceSerializer = DI.get(AccountPreferenceSerializer.class);
|
||||
LocalStoreProvider localStoreProvider = DI.get(LocalStoreProvider.class);
|
||||
StoragePersister storagePersister = DI.get(StoragePersister.class);
|
||||
|
||||
preferences = new Preferences(appContext, resourceProvider, storagePersister, localStoreProvider, localKeyStoreManager, accountPreferenceSerializer);
|
||||
}
|
||||
return preferences;
|
||||
}
|
||||
|
@ -49,24 +52,35 @@ public class Preferences {
|
|||
private final LocalStoreProvider localStoreProvider;
|
||||
private final CoreResourceProvider resourceProvider;
|
||||
private final LocalKeyStoreManager localKeyStoreManager;
|
||||
private final StoragePersister storagePersister;
|
||||
|
||||
private Preferences(Context context, CoreResourceProvider resourceProvider,
|
||||
LocalStoreProvider localStoreProvider, LocalKeyStoreManager localKeyStoreManager,
|
||||
StoragePersister storagePersister, LocalStoreProvider localStoreProvider,
|
||||
LocalKeyStoreManager localKeyStoreManager,
|
||||
AccountPreferenceSerializer accountPreferenceSerializer) {
|
||||
storage = Storage.getStorage(context);
|
||||
this.storage = new Storage();
|
||||
this.storagePersister = storagePersister;
|
||||
this.context = context;
|
||||
this.resourceProvider = resourceProvider;
|
||||
this.localStoreProvider = localStoreProvider;
|
||||
this.localKeyStoreManager = localKeyStoreManager;
|
||||
this.accountPreferenceSerializer = accountPreferenceSerializer;
|
||||
|
||||
Map<String, String> persistedStorageValues = storagePersister.loadValues();
|
||||
storage.replaceAll(persistedStorageValues);
|
||||
|
||||
if (storage.isEmpty()) {
|
||||
Timber.i("Preferences storage is zero-size, importing from Android-style preferences");
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = createStorageEditor();
|
||||
editor.copy(context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE));
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
|
||||
public StorageEditor createStorageEditor() {
|
||||
return new StorageEditor(storage, storagePersister);
|
||||
}
|
||||
|
||||
@RestrictTo(Scope.TESTS)
|
||||
public void clearAccounts() {
|
||||
accounts = new HashMap<>();
|
||||
|
@ -160,7 +174,9 @@ public class Preferences {
|
|||
}
|
||||
LocalStore.removeAccount(account);
|
||||
|
||||
DI.get(AccountPreferenceSerializer.class).delete(storage, account);
|
||||
StorageEditor storageEditor = createStorageEditor();
|
||||
accountPreferenceSerializer.delete(storageEditor, storage, account);
|
||||
storageEditor.commit();
|
||||
localKeyStoreManager.deleteCertificates(account);
|
||||
|
||||
if (newAccount == account) {
|
||||
|
@ -189,7 +205,7 @@ public class Preferences {
|
|||
}
|
||||
|
||||
public void setDefaultAccount(Account account) {
|
||||
getStorage().edit().putString("defaultAccountUuid", account.getUuid()).commit();
|
||||
createStorageEditor().putString("defaultAccountUuid", account.getUuid()).commit();
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
|
@ -201,11 +217,12 @@ public class Preferences {
|
|||
}
|
||||
|
||||
public void saveAccount(Account account) {
|
||||
StorageEditor editor = storage.edit();
|
||||
|
||||
ensureAssignedAccountNumber(account);
|
||||
processChangedValues(account);
|
||||
accountPreferenceSerializer.save(storage, editor, account);
|
||||
|
||||
StorageEditor editor = createStorageEditor();
|
||||
accountPreferenceSerializer.save(editor, storage, account);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void ensureAssignedAccountNumber(Account account) {
|
||||
|
@ -265,7 +282,9 @@ public class Preferences {
|
|||
}
|
||||
|
||||
public void move(Account account, boolean mUp) {
|
||||
accountPreferenceSerializer.move(account, storage, mUp);
|
||||
StorageEditor storageEditor = createStorageEditor();
|
||||
accountPreferenceSerializer.move(storageEditor, account, storage, mUp);
|
||||
storageEditor.commit();
|
||||
loadAccounts();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import android.support.annotation.NonNull;
|
|||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.controller.MessageReference;
|
||||
import com.fsck.k9.backend.api.MessageRemovalListener;
|
||||
import com.fsck.k9.crypto.EncryptionExtractor;
|
||||
|
@ -630,7 +631,7 @@ public class LocalFolder extends Folder<LocalMessage> {
|
|||
public void delete() throws MessagingException {
|
||||
String id = getPrefId();
|
||||
|
||||
StorageEditor editor = this.localStore.getStorage().edit();
|
||||
StorageEditor editor = localStore.getPreferences().createStorageEditor();
|
||||
|
||||
editor.remove(id + ".displayMode");
|
||||
editor.remove(id + ".syncMode");
|
||||
|
@ -642,7 +643,7 @@ public class LocalFolder extends Folder<LocalMessage> {
|
|||
}
|
||||
|
||||
public void save() throws MessagingException {
|
||||
StorageEditor editor = this.localStore.getStorage().edit();
|
||||
StorageEditor editor = localStore.getPreferences().createStorageEditor();
|
||||
save(editor);
|
||||
editor.commit();
|
||||
}
|
||||
|
|
|
@ -244,6 +244,10 @@ public class LocalStore {
|
|||
return Preferences.getPreferences(context).getStorage();
|
||||
}
|
||||
|
||||
protected Preferences getPreferences() {
|
||||
return Preferences.getPreferences(context);
|
||||
}
|
||||
|
||||
public long getSize() throws MessagingException {
|
||||
|
||||
final StorageManager storageManager = StorageManager.getInstance(context);
|
||||
|
@ -1336,6 +1340,11 @@ public class LocalStore {
|
|||
return LocalStore.this.getStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Preferences getPreferences() {
|
||||
return LocalStore.this.getPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return LocalStore.this.getAccount();
|
||||
|
|
|
@ -6,8 +6,8 @@ import java.util.List;
|
|||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
import com.fsck.k9.mailstore.LocalStore;
|
||||
import com.fsck.k9.preferences.Storage;
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ import com.fsck.k9.preferences.Storage;
|
|||
public interface MigrationsHelper {
|
||||
LocalStore getLocalStore();
|
||||
Storage getStorage();
|
||||
Preferences getPreferences();
|
||||
Account getAccount();
|
||||
Context getContext();
|
||||
String serializeFlags(List<Flag> flags);
|
||||
|
|
|
@ -179,7 +179,7 @@ public class SettingsImporter {
|
|||
|
||||
if (globalSettings) {
|
||||
try {
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
if (imported.globalSettings != null) {
|
||||
importGlobalSettings(storage, editor, imported.contentVersion, imported.globalSettings);
|
||||
} else {
|
||||
|
@ -202,7 +202,7 @@ public class SettingsImporter {
|
|||
if (imported.accounts.containsKey(accountUuid)) {
|
||||
ImportedAccount account = imported.accounts.get(accountUuid);
|
||||
try {
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
|
||||
AccountDescriptionPair importResult = importAccount(context, editor,
|
||||
imported.contentVersion, account, overwrite);
|
||||
|
@ -214,7 +214,7 @@ public class SettingsImporter {
|
|||
// Add UUID of the account we just imported to the list of
|
||||
// account UUIDs
|
||||
if (!importResult.overwritten) {
|
||||
editor = storage.edit();
|
||||
editor = preferences.createStorageEditor();
|
||||
|
||||
String newUuid = importResult.imported.uuid;
|
||||
String oldAccountUuids = storage.getString("accountUuids", "");
|
||||
|
@ -253,7 +253,7 @@ public class SettingsImporter {
|
|||
}
|
||||
}
|
||||
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
|
||||
String defaultAccountUuid = storage.getString("defaultAccountUuid", null);
|
||||
if (defaultAccountUuid == null) {
|
||||
|
|
|
@ -1,199 +1,24 @@
|
|||
package com.fsck.k9.preferences;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrations;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrationsHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Storage {
|
||||
private static ConcurrentMap<Context, Storage> storages = new ConcurrentHashMap<>();
|
||||
private HashMap<String, String> storage = new HashMap<>();
|
||||
|
||||
private volatile ConcurrentMap<String, String> storage = new ConcurrentHashMap<>();
|
||||
|
||||
private static final int DB_VERSION = 4;
|
||||
private static final String DB_NAME = "preferences_storage";
|
||||
|
||||
private ThreadLocal<ConcurrentMap<String, String>> workingStorage = new ThreadLocal<>();
|
||||
private ThreadLocal<SQLiteDatabase> workingDB = new ThreadLocal<>();
|
||||
private ThreadLocal<List<String>> workingChangedKeys = new ThreadLocal<>();
|
||||
|
||||
|
||||
private Context context = null;
|
||||
|
||||
private SQLiteDatabase openDB() {
|
||||
SQLiteDatabase db = context.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (db.getVersion() < 1) {
|
||||
createStorageDatabase(db);
|
||||
} else {
|
||||
StorageMigrations.upgradeDatabase(db, migrationsHelper);
|
||||
}
|
||||
|
||||
db.setVersion(DB_VERSION);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (db.getVersion() != DB_VERSION) {
|
||||
throw new RuntimeException("Storage database upgrade failed!");
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
private void createStorageDatabase(SQLiteDatabase db) {
|
||||
Timber.i("Creating Storage database");
|
||||
|
||||
db.execSQL("DROP TABLE IF EXISTS preferences_storage");
|
||||
db.execSQL("CREATE TABLE preferences_storage " +
|
||||
"(primkey TEXT PRIMARY KEY ON CONFLICT REPLACE, value TEXT)");
|
||||
db.setVersion(DB_VERSION);
|
||||
}
|
||||
|
||||
public static Storage getStorage(Context context) {
|
||||
Storage tmpStorage = storages.get(context);
|
||||
if (tmpStorage != null) {
|
||||
Timber.d("Returning already existing Storage");
|
||||
return tmpStorage;
|
||||
} else {
|
||||
Timber.d("Creating provisional storage");
|
||||
tmpStorage = new Storage(context);
|
||||
Storage oldStorage = storages.putIfAbsent(context, tmpStorage);
|
||||
if (oldStorage != null) {
|
||||
Timber.d("Another thread beat us to creating the Storage, returning that one");
|
||||
return oldStorage;
|
||||
} else {
|
||||
Timber.d("Returning the Storage we created");
|
||||
return tmpStorage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadValues() {
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Loading preferences from DB into Storage");
|
||||
Cursor cursor = null;
|
||||
SQLiteDatabase mDb = null;
|
||||
try {
|
||||
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);
|
||||
Timber.d("Loading key '%s', value = '%s'", key, value);
|
||||
storage.put(key, value);
|
||||
}
|
||||
} finally {
|
||||
Utility.closeQuietly(cursor);
|
||||
if (mDb != null) {
|
||||
mDb.close();
|
||||
}
|
||||
long endTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Preferences load took %d ms", endTime - startTime);
|
||||
}
|
||||
}
|
||||
|
||||
private Storage(Context context) {
|
||||
this.context = context;
|
||||
loadValues();
|
||||
}
|
||||
|
||||
private void keyChange(String key) {
|
||||
List<String> changedKeys = workingChangedKeys.get();
|
||||
if (!changedKeys.contains(key)) {
|
||||
changedKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
void put(Map<String, String> insertables) {
|
||||
String sql = "INSERT INTO preferences_storage (primkey, value) VALUES (?, ?)";
|
||||
SQLiteStatement stmt = workingDB.get().compileStatement(sql);
|
||||
|
||||
for (Map.Entry<String, String> entry : insertables.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
stmt.bindString(1, key);
|
||||
stmt.bindString(2, value);
|
||||
stmt.execute();
|
||||
stmt.clearBindings();
|
||||
liveUpdate(key, value);
|
||||
}
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
private void liveUpdate(String key, String value) {
|
||||
workingStorage.get().put(key, value);
|
||||
|
||||
keyChange(key);
|
||||
}
|
||||
|
||||
void remove(String key) {
|
||||
workingDB.get().delete("preferences_storage", "primkey = ?", new String[] { key });
|
||||
workingStorage.get().remove(key);
|
||||
|
||||
keyChange(key);
|
||||
}
|
||||
|
||||
void doInTransaction(Runnable dbWork) {
|
||||
ConcurrentMap<String, String> newStorage = new ConcurrentHashMap<>(storage);
|
||||
workingStorage.set(newStorage);
|
||||
|
||||
SQLiteDatabase mDb = openDB();
|
||||
workingDB.set(mDb);
|
||||
|
||||
List<String> changedKeys = new ArrayList<>();
|
||||
workingChangedKeys.set(changedKeys);
|
||||
|
||||
mDb.beginTransaction();
|
||||
try {
|
||||
dbWork.run();
|
||||
mDb.setTransactionSuccessful();
|
||||
storage = newStorage;
|
||||
} finally {
|
||||
workingDB.remove();
|
||||
workingStorage.remove();
|
||||
workingChangedKeys.remove();
|
||||
mDb.endTransaction();
|
||||
mDb.close();
|
||||
}
|
||||
}
|
||||
public Storage() { }
|
||||
|
||||
public boolean isEmpty() {
|
||||
return storage.isEmpty();
|
||||
}
|
||||
|
||||
public boolean contains(String key) {
|
||||
// TODO this used to be ConcurrentHashMap#contains which is
|
||||
// actually containsValue. But looking at the usage of this method,
|
||||
// it's clear that containsKey is what's intended. Investigate if this
|
||||
// was a bug previously. Looks like it was only used once, when upgrading
|
||||
return storage.containsKey(key);
|
||||
}
|
||||
|
||||
public StorageEditor edit() {
|
||||
return new StorageEditor(this);
|
||||
}
|
||||
|
||||
public Map<String, String> getAll() {
|
||||
return storage;
|
||||
}
|
||||
|
@ -240,77 +65,8 @@ public class Storage {
|
|||
return val;
|
||||
}
|
||||
|
||||
private String readValue(SQLiteDatabase mDb, String key) {
|
||||
Cursor cursor = null;
|
||||
String value = null;
|
||||
try {
|
||||
cursor = mDb.query(
|
||||
"preferences_storage",
|
||||
new String[] {"value"},
|
||||
"primkey = ?",
|
||||
new String[] {key},
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
value = cursor.getString(0);
|
||||
Timber.d("Loading key '%s', value = '%s'", key, value);
|
||||
}
|
||||
} finally {
|
||||
Utility.closeQuietly(cursor);
|
||||
}
|
||||
|
||||
return value;
|
||||
public void replaceAll(Map<String, String> workingStorage) {
|
||||
storage.clear();
|
||||
storage.putAll(workingStorage);
|
||||
}
|
||||
|
||||
private void writeValue(SQLiteDatabase mDb, String key, String value) {
|
||||
if (value == null) {
|
||||
mDb.delete("preferences_storage", "primkey = ?", new String[] { key });
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("primkey", key);
|
||||
cv.put("value", value);
|
||||
|
||||
long result = mDb.update("preferences_storage", cv, "primkey = ?", new String[] { key });
|
||||
|
||||
if (result == -1) {
|
||||
Timber.e("Error writing key '%s', value = '%s'", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertValue(SQLiteDatabase mDb, String key, String value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("primkey", key);
|
||||
cv.put("value", value);
|
||||
|
||||
long result = mDb.insert("preferences_storage", null, cv);
|
||||
|
||||
if (result == -1) {
|
||||
Timber.e("Error writing key '%s', value = '%s'", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private StorageMigrationsHelper migrationsHelper = new StorageMigrationsHelper() {
|
||||
@Override
|
||||
public void writeValue(@NotNull SQLiteDatabase db, @NotNull String key, String value) {
|
||||
Storage.this.writeValue(db, key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readValue(@NotNull SQLiteDatabase db, @NotNull String key) {
|
||||
return Storage.this.readValue(db, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertValue(@NotNull SQLiteDatabase db, @NotNull String key, @Nullable String value) {
|
||||
Storage.this.insertValue(db, key, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,19 +9,23 @@ import java.util.Map.Entry;
|
|||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.fsck.k9.preferences.StoragePersister.StoragePersistOperationCallback;
|
||||
import com.fsck.k9.preferences.StoragePersister.StoragePersistOperations;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class StorageEditor {
|
||||
private Storage storage;
|
||||
private StoragePersister storagePersister;
|
||||
|
||||
private Map<String, String> changes = new HashMap<>();
|
||||
private List<String> removals = new ArrayList<>();
|
||||
|
||||
Map<String, String> snapshot = new HashMap<>();
|
||||
private Map<String, String> snapshot = new HashMap<>();
|
||||
|
||||
|
||||
StorageEditor(Storage storage) {
|
||||
public StorageEditor(Storage storage, StoragePersister storagePersister) {
|
||||
this.storage = storage;
|
||||
this.storagePersister = storagePersister;
|
||||
snapshot.putAll(storage.getAll());
|
||||
}
|
||||
|
||||
|
@ -52,10 +56,16 @@ public class StorageEditor {
|
|||
private void commitChanges() {
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Committing preference changes");
|
||||
Runnable committer = new Runnable() {
|
||||
public void run() {
|
||||
StoragePersistOperationCallback committer = new StoragePersistOperationCallback() {
|
||||
@Override
|
||||
public void beforePersistTransaction(Map<String, String> workingStorage) {
|
||||
workingStorage.putAll(storage.getAll());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist(StoragePersistOperations ops) {
|
||||
for (String removeKey : removals) {
|
||||
storage.remove(removeKey);
|
||||
ops.remove(removeKey);
|
||||
}
|
||||
Map<String, String> insertables = new HashMap<>();
|
||||
for (Entry<String, String> entry : changes.entrySet()) {
|
||||
|
@ -66,13 +76,17 @@ public class StorageEditor {
|
|||
insertables.put(key, newValue);
|
||||
}
|
||||
}
|
||||
storage.put(insertables);
|
||||
ops.put(insertables);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPersistTransactionSuccess(Map<String, String> workingStorage) {
|
||||
storage.replaceAll(workingStorage);
|
||||
}
|
||||
};
|
||||
storage.doInTransaction(committer);
|
||||
storagePersister.doInTransaction(committer);
|
||||
long endTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Preferences commit took %d ms", endTime - startTime);
|
||||
|
||||
}
|
||||
|
||||
public StorageEditor putBoolean(String key,
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
package com.fsck.k9.preferences;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.CheckResult;
|
||||
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrations;
|
||||
import com.fsck.k9.preferences.migrations.StorageMigrationsHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
public class StoragePersister {
|
||||
private static final int DB_VERSION = 4;
|
||||
private static final String DB_NAME = "preferences_storage";
|
||||
|
||||
private final Context context;
|
||||
|
||||
public StoragePersister(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private SQLiteDatabase openDB() {
|
||||
SQLiteDatabase db = context.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (db.getVersion() < 1) {
|
||||
createStorageDatabase(db);
|
||||
} else {
|
||||
StorageMigrations.upgradeDatabase(db, migrationsHelper);
|
||||
}
|
||||
|
||||
db.setVersion(DB_VERSION);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (db.getVersion() != DB_VERSION) {
|
||||
throw new RuntimeException("Storage database upgrade failed!");
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
private void createStorageDatabase(SQLiteDatabase db) {
|
||||
Timber.i("Creating Storage database");
|
||||
|
||||
db.execSQL("DROP TABLE IF EXISTS preferences_storage");
|
||||
db.execSQL("CREATE TABLE preferences_storage " +
|
||||
"(primkey TEXT PRIMARY KEY ON CONFLICT REPLACE, value TEXT)");
|
||||
db.setVersion(DB_VERSION);
|
||||
}
|
||||
|
||||
void doInTransaction(StoragePersistOperationCallback operationCallback) {
|
||||
HashMap<String, String> workingStorage = new HashMap<>();
|
||||
SQLiteDatabase workingDb = openDB();
|
||||
|
||||
try {
|
||||
operationCallback.beforePersistTransaction(workingStorage);
|
||||
|
||||
StoragePersistOperations storagePersistOperations = new StoragePersistOperations(workingStorage, workingDb);
|
||||
workingDb.beginTransaction();
|
||||
operationCallback.persist(storagePersistOperations);
|
||||
workingDb.setTransactionSuccessful();
|
||||
|
||||
operationCallback.onPersistTransactionSuccess(workingStorage);
|
||||
} finally {
|
||||
workingDb.endTransaction();
|
||||
workingDb.close();
|
||||
}
|
||||
}
|
||||
|
||||
static class StoragePersistOperations {
|
||||
private SQLiteDatabase workingDB;
|
||||
private Map<String, String> workingStorage;
|
||||
|
||||
private StoragePersistOperations(Map<String, String> workingStorage, SQLiteDatabase workingDb) {
|
||||
this.workingDB = workingDb;
|
||||
this.workingStorage = workingStorage;
|
||||
}
|
||||
|
||||
void put(Map<String, String> insertables) {
|
||||
String sql = "INSERT INTO preferences_storage (primkey, value) VALUES (?, ?)";
|
||||
SQLiteStatement stmt = workingDB.compileStatement(sql);
|
||||
|
||||
for (Map.Entry<String, String> entry : insertables.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
stmt.bindString(1, key);
|
||||
stmt.bindString(2, value);
|
||||
stmt.execute();
|
||||
stmt.clearBindings();
|
||||
liveUpdate(key, value);
|
||||
}
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
private void liveUpdate(String key, String value) {
|
||||
workingStorage.put(key, value);
|
||||
}
|
||||
|
||||
void remove(String key) {
|
||||
workingDB.delete("preferences_storage", "primkey = ?", new String[] { key });
|
||||
workingStorage.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
interface StoragePersistOperationCallback {
|
||||
void beforePersistTransaction(Map<String, String> workingStorage);
|
||||
void persist(StoragePersistOperations ops);
|
||||
void onPersistTransactionSuccess(Map<String, String> workingStorage);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public Map<String, String> loadValues() {
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Loading preferences from DB into Storage");
|
||||
HashMap<String, String> loadedValues = new HashMap<>();
|
||||
Cursor cursor = null;
|
||||
SQLiteDatabase mDb = null;
|
||||
try {
|
||||
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);
|
||||
Timber.d("Loading key '%s', value = '%s'", key, value);
|
||||
loadedValues.put(key, value);
|
||||
}
|
||||
} finally {
|
||||
Utility.closeQuietly(cursor);
|
||||
if (mDb != null) {
|
||||
mDb.close();
|
||||
}
|
||||
long endTime = SystemClock.elapsedRealtime();
|
||||
Timber.i("Preferences load took %d ms", endTime - startTime);
|
||||
}
|
||||
|
||||
return loadedValues;
|
||||
}
|
||||
|
||||
private String readValue(SQLiteDatabase mDb, String key) {
|
||||
Cursor cursor = null;
|
||||
String value = null;
|
||||
try {
|
||||
cursor = mDb.query(
|
||||
"preferences_storage",
|
||||
new String[] {"value"},
|
||||
"primkey = ?",
|
||||
new String[] {key},
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
value = cursor.getString(0);
|
||||
Timber.d("Loading key '%s', value = '%s'", key, value);
|
||||
}
|
||||
} finally {
|
||||
Utility.closeQuietly(cursor);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private void writeValue(SQLiteDatabase mDb, String key, String value) {
|
||||
if (value == null) {
|
||||
mDb.delete("preferences_storage", "primkey = ?", new String[] { key });
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("primkey", key);
|
||||
cv.put("value", value);
|
||||
|
||||
long result = mDb.update("preferences_storage", cv, "primkey = ?", new String[] { key });
|
||||
|
||||
if (result == -1) {
|
||||
Timber.e("Error writing key '%s', value = '%s'", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertValue(SQLiteDatabase mDb, String key, String value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("primkey", key);
|
||||
cv.put("value", value);
|
||||
|
||||
long result = mDb.insert("preferences_storage", null, cv);
|
||||
|
||||
if (result == -1) {
|
||||
Timber.e("Error writing key '%s', value = '%s'", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private StorageMigrationsHelper migrationsHelper = new StorageMigrationsHelper() {
|
||||
@Override
|
||||
public void writeValue(@NotNull SQLiteDatabase db, @NotNull String key, String value) {
|
||||
StoragePersister.this.writeValue(db, key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readValue(@NotNull SQLiteDatabase db, @NotNull String key) {
|
||||
return StoragePersister.this.readValue(db, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertValue(@NotNull SQLiteDatabase db, @NotNull String key, @Nullable String value) {
|
||||
StoragePersister.this.insertValue(db, key, value);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -51,7 +51,7 @@ public class MessageProviderTest extends ProviderTestCase2 {
|
|||
|
||||
private void createAccount() {
|
||||
Preferences preferences = Preferences.getPreferences(getMockContext());
|
||||
Account account = preferences.newAccount();
|
||||
Account account = preferences.createEmptyAccount();
|
||||
account.setDescription("TestAccount");
|
||||
account.setChipColor(10);
|
||||
account.setStoreUri("imap://user@domain.com/");
|
||||
|
|
|
@ -100,7 +100,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void query_forMessagesWithAccountAndWithoutRequiredFields_throwsIllegalArgumentException() {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = getProvider().query(
|
||||
|
@ -116,7 +116,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test(expected = SQLException.class) //Handle this better?
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsWithNoOrderBy_throwsSQLiteException() {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = getProvider().query(
|
||||
|
@ -136,7 +136,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forMessagesWithEmptyAccountAndRequiredFieldsAndOrderBy_providesEmptyResult() {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = getProvider().query(
|
||||
|
@ -157,7 +157,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_providesResult() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
DI.get(LocalStoreProvider.class).getInstance(account).getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
|
||||
|
@ -179,7 +179,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_sortsCorrectly() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
DI.get(LocalStoreProvider.class).getInstance(account)
|
||||
.getFolder("Inbox").appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
@ -205,7 +205,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forThreadedMessages_sortsCorrectly() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
DI.get(LocalStoreProvider.class).getInstance(account)
|
||||
.getFolder("Inbox").appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
@ -232,7 +232,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forThreadedMessages_showsThreadOfEmailOnce() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
DI.get(LocalStoreProvider.class).getInstance(account).getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
DI.get(LocalStoreProvider.class).getInstance(account).getFolder("Inbox").appendMessages(Collections.singletonList(reply));
|
||||
|
@ -260,7 +260,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forThreadedMessages_showsThreadOfEmailWithSameSendTimeOnce() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
DI.get(LocalStoreProvider.class).getInstance(account).getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
DI.get(LocalStoreProvider.class).getInstance(account).getFolder("Inbox").appendMessages(Collections.singletonList(replyAtSameTime));
|
||||
|
@ -288,7 +288,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void query_forAThreadOfMessages_returnsMessage() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
Account account = Preferences.getPreferences(getContext()).createEmptyAccount();
|
||||
account.getUuid();
|
||||
Message message = new MimeMessage();
|
||||
message.setSubject("Test Subject");
|
||||
|
|
|
@ -139,9 +139,7 @@ public class RemoteControlService extends CoreService {
|
|||
K9.setK9Theme(K9RemoteControl.K9_THEME_DARK.equals(theme) ? K9.Theme.DARK : K9.Theme.LIGHT);
|
||||
}
|
||||
|
||||
Storage storage = preferences.getStorage();
|
||||
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
K9.save(editor);
|
||||
editor.commit();
|
||||
|
||||
|
|
|
@ -18,10 +18,9 @@ class MigrationTo42 {
|
|||
public static void from41MoveFolderPreferences(MigrationsHelper migrationsHelper) {
|
||||
try {
|
||||
LocalStore localStore = migrationsHelper.getLocalStore();
|
||||
Storage storage = migrationsHelper.getStorage();
|
||||
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = migrationsHelper.getPreferences().createStorageEditor();
|
||||
|
||||
List<? extends Folder > folders = localStore.getPersonalNamespaces(true);
|
||||
for (Folder folder : folders) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import android.database.sqlite.SQLiteDatabase;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.core.BuildConfig;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
@ -339,6 +339,11 @@ public class StoreSchemaDefinitionTest extends K9RobolectricTest {
|
|||
return localStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Preferences getPreferences() {
|
||||
return mock(Preferences.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Storage getStorage() {
|
||||
return mock(Storage.class);
|
||||
|
|
|
@ -1611,7 +1611,7 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
|
|||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
StorageEditor editor = preferences.getStorage().edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
K9.save(editor);
|
||||
editor.commit();
|
||||
}
|
||||
|
|
|
@ -11,9 +11,8 @@ import com.fsck.k9.FontSizes;
|
|||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.activity.K9PreferenceActivity;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.preferences.Storage;
|
||||
import com.fsck.k9.preferences.StorageEditor;
|
||||
import com.fsck.k9.ui.R;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -191,8 +190,7 @@ public class FontSizeSettings extends K9PreferenceActivity {
|
|||
|
||||
fontSizes.setMessageComposeInput(Integer.parseInt(mMessageComposeInput.getValue()));
|
||||
|
||||
Storage storage = Preferences.getPreferences(this).getStorage();
|
||||
StorageEditor editor = storage.edit();
|
||||
StorageEditor editor = Preferences.getPreferences(this).createStorageEditor();
|
||||
fontSizes.save(editor);
|
||||
editor.commit();
|
||||
}
|
||||
|
|
|
@ -850,7 +850,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
|
|||
K9.setSortAscending(this.sortType, this.sortAscending);
|
||||
sortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
|
||||
|
||||
StorageEditor editor = preferences.getStorage().edit();
|
||||
StorageEditor editor = preferences.createStorageEditor();
|
||||
K9.save(editor);
|
||||
editor.commit();
|
||||
}
|
||||
|
|
|
@ -205,7 +205,7 @@ class GeneralSettingsDataStore(
|
|||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
val editor = preferences.storage.edit()
|
||||
val editor = preferences.createStorageEditor()
|
||||
K9.save(editor)
|
||||
|
||||
executorService.execute {
|
||||
|
|
Loading…
Reference in a new issue