diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1772e9d20..a67c1f54c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -421,5 +421,13 @@ otherwise it would make K-9 start at the wrong time
+
+
+
+
diff --git a/res/layout/upgrade_databases.xml b/res/layout/upgrade_databases.xml
new file mode 100644
index 000000000..fdc90ae9f
--- /dev/null
+++ b/res/layout/upgrade_databases.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3ddb0419a..cfdaa4627 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1093,4 +1093,8 @@ http://k9mail.googlecode.com/
Threaded view
Group messages by conversation
+
+ Upgrading databases
+ Upgrading databases…
+ Upgrading database of account \"%s\"
diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java
index 46403e109..12948202b 100644
--- a/src/com/fsck/k9/K9.java
+++ b/src/com/fsck/k9/K9.java
@@ -15,6 +15,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -27,12 +28,14 @@ import android.util.Log;
import com.fsck.k9.Account.SortType;
import com.fsck.k9.activity.MessageCompose;
+import com.fsck.k9.activity.UpgradeDatabases;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
+import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.provider.UnreadWidgetProvider;
import com.fsck.k9.service.BootReceiver;
import com.fsck.k9.service.MailService;
@@ -64,6 +67,23 @@ public class K9 extends Application {
public static File tempDirectory;
public static final String LOG_TAG = "k9";
+ /**
+ * Name of the {@link SharedPreferences} file used to store the last known version of the
+ * accounts' databases.
+ *
+ *
+ * See {@link UpgradeDatabases} for a detailed explanation of the database upgrade process.
+ *
+ */
+ private static final String DATABASE_VERSION_CACHE = "database_version_cache";
+
+ /**
+ * Key used to store the last known database version of the accounts' databases.
+ *
+ * @see #DATABASE_VERSION_CACHE
+ */
+ private static final String KEY_LAST_ACCOUNT_DATABASE_VERSION = "last_account_database_version";
+
/**
* Components that are interested in knowing when the K9 instance is
* available and ready.
@@ -150,6 +170,16 @@ public class K9 extends Application {
public static boolean ENABLE_ERROR_FOLDER = true;
public static String ERROR_FOLDER_NAME = "K9mail-errors";
+ /**
+ * A reference to the {@link SharedPreferences} used for caching the last known database
+ * version.
+ *
+ * @see #checkCachedDatabaseVersion()
+ * @see #setDatabasesUpToDate(boolean)
+ */
+ private static SharedPreferences sDatabaseVersionCache;
+
+
private static boolean mAnimations = true;
private static boolean mConfirmDelete = false;
@@ -208,6 +238,11 @@ public class K9 extends Application {
private static boolean sUseBackgroundAsUnreadIndicator = true;
private static boolean sThreadedViewEnabled = true;
+ /**
+ * @see #areDatabasesUpToDate()
+ */
+ private static boolean sDatabasesUpToDate = false;
+
/**
* The MIME type(s) of attachments we're willing to view.
@@ -487,6 +522,8 @@ public class K9 extends Application {
galleryBuggy = checkForBuggyGallery();
+ checkCachedDatabaseVersion();
+
Preferences prefs = Preferences.getPreferences(this);
loadPrefs(prefs);
@@ -581,6 +618,31 @@ public class K9 extends Application {
notifyObservers();
}
+ /**
+ * Loads the last known database version of the accounts' databases from a
+ * {@link SharedPreference}.
+ *
+ *
+ * If the stored version matches {@link LocalStore#DB_VERSION} we know that the databases are
+ * up to date.
+ * Using {@code SharedPreferences} should be a lot faster than opening all SQLite databases to
+ * get the current database version.
+ *
+ * See {@link UpgradeDatabases} for a detailed explanation of the database upgrade process.
+ *
+ *
+ * @see #areDatabasesUpToDate()
+ */
+ public void checkCachedDatabaseVersion() {
+ sDatabaseVersionCache = getSharedPreferences(DATABASE_VERSION_CACHE, MODE_PRIVATE);
+
+ int cachedVersion = sDatabaseVersionCache.getInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, 0);
+
+ if (cachedVersion < LocalStore.DB_VERSION) {
+ K9.setDatabasesUpToDate(false);
+ }
+ }
+
public static void loadPrefs(Preferences prefs) {
SharedPreferences sprefs = prefs.getPreferences();
DEBUG = sprefs.getBoolean("enableDebugLogging", false);
@@ -1150,4 +1212,38 @@ public class K9 extends Application {
public static synchronized void setThreadedViewEnabled(boolean enable) {
sThreadedViewEnabled = enable;
}
+
+ /**
+ * Check if we already know whether all databases are using the current database schema.
+ *
+ *
+ * This method is only used for optimizations. If it returns {@code true} we can be certain that
+ * getting a {@link LocalStore} instance won't trigger a schema upgrade.
+ *
+ *
+ * @return {@code true}, if we know that all databases are using the current database schema.
+ * {@code false}, otherwise.
+ */
+ public static synchronized boolean areDatabasesUpToDate() {
+ return sDatabasesUpToDate;
+ }
+
+ /**
+ * Remember that all account databases are using the most recent database schema.
+ *
+ * @param save
+ * Whether or not to write the current database version to the
+ * {@code SharedPreferences} {@link #DATABASE_VERSION_CACHE}.
+ *
+ * @see #areDatabasesUpToDate()
+ */
+ public static synchronized void setDatabasesUpToDate(boolean save) {
+ sDatabasesUpToDate = true;
+
+ if (save) {
+ Editor editor = sDatabaseVersionCache.edit();
+ editor.putInt(KEY_LAST_ACCOUNT_DATABASE_VERSION, LocalStore.DB_VERSION);
+ editor.commit();
+ }
+ }
}
diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index 5e04a6ede..34fc85dd8 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -371,6 +371,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
if (accounts.length < 1) {
WelcomeMessage.showWelcomeMessage(this);
finish();
+ return;
+ }
+
+ if (UpgradeDatabases.actionUpgradeDatabases(this, intent)) {
+ finish();
+ return;
}
boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true);
diff --git a/src/com/fsck/k9/activity/UpgradeDatabases.java b/src/com/fsck/k9/activity/UpgradeDatabases.java
new file mode 100644
index 000000000..3fd46723d
--- /dev/null
+++ b/src/com/fsck/k9/activity/UpgradeDatabases.java
@@ -0,0 +1,220 @@
+package com.fsck.k9.activity;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.R;
+import com.fsck.k9.mail.Store;
+import com.fsck.k9.service.DatabaseUpgradeService;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.widget.TextView;
+
+
+/**
+ * This activity triggers a database upgrade if necessary and displays the current upgrade progress.
+ *
+ *
+ * The current upgrade process works as follows:
+ *
+ * - Activities that access an account's database call
+ * {@link #actionUpgradeDatabases(Context, Intent)} in their {@link Activity#onCreate(Bundle)}
+ * method.
+ * - {@link #actionUpgradeDatabases(Context, Intent)} will call {@link K9#areDatabasesUpToDate()}
+ * to check if we already know whether the databases have been upgraded.
+ * - {@link K9#areDatabasesUpToDate()} will compare the last known database version stored in a
+ * {@link SharedPreferences} file to {@link com.fsck.k9.mail.store.LocalStore#DB_VERSION}. This
+ * is done as an optimization because it's faster than opening all of the accounts' databases
+ * one by one.
+ * - If there was an error reading the cached database version or if it shows the databases need
+ * upgrading this activity ({@code UpgradeDatabases}) is started.
+ * - This activity will display a spinning progress indicator and start
+ * {@link DatabaseUpgradeService}.
+ * - {@link DatabaseUpgradeService} will acquire a partial wake lock (with a 10 minute timeout),
+ * start a background thread to perform the database upgrades, and report the progress using
+ * {@link LocalBroadcastManager} to this activity which will update the UI accordingly.
+ * - Once the upgrade is complete {@link DatabaseUpgradeService} will notify this activity,
+ * release the wake lock, and stop itself.
+ * - This activity will start the original activity using the intent supplied when calling
+ * {@link #actionUpgradeDatabases(Context, Intent)}.
+ *
+ *
+ * Currently we make no attempts to stop the background code (e.g. {@link MessagingController}) from
+ * opening the accounts' databases. If this happens the upgrade is performed in one of the
+ * background threads and not by {@link DatabaseUpgradeService}. But this is not a problem. Due to
+ * the locking in {@link Store#getLocalInstance(Account, android.app.Application)} the upgrade
+ * service will block in the {@link Account#getLocalStore()} call and from the outside (especially
+ * for this activity) it will appear as if {@link DatabaseUpgradeService} is performing the upgrade.
+ *
+ */
+public class UpgradeDatabases extends K9Activity {
+ private static final String ACTION_UPGRADE_DATABASES = "upgrade_databases";
+ private static final String EXTRA_START_INTENT = "start_intent";
+
+
+ /**
+ * Start the {@link UpgradeDatabases} activity if necessary.
+ *
+ * @param context
+ * The {@link Context} used to start the activity.
+ * @param startIntent
+ * After the database upgrade is complete an activity is started using this intent.
+ * Usually this is the intent that was used to start the calling activity.
+ * Never {@code null}.
+ *
+ * @return {@code true}, if the {@code UpgradeDatabases} activity was started. In this case the
+ * calling activity is expected to finish itself.
+ * {@code false}, if the account databases don't need upgrading.
+ */
+ public static boolean actionUpgradeDatabases(Context context, Intent startIntent) {
+ if (K9.areDatabasesUpToDate()) {
+ return false;
+ }
+
+ Intent intent = new Intent(context, UpgradeDatabases.class);
+ intent.setAction(ACTION_UPGRADE_DATABASES);
+ intent.putExtra(EXTRA_START_INTENT, startIntent);
+
+ // Make sure this activity is only running once
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ context.startActivity(intent);
+ return true;
+ }
+
+
+ private Intent mStartIntent;
+
+ private TextView mUpgradeText;
+
+ private LocalBroadcastManager mLocalBroadcastManager;
+ private UpgradeDatabaseBroadcastReceiver mBroadcastReceiver;
+ private IntentFilter mIntentFilter;
+ private Preferences mPreferences;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // If the databases have already been upgraded there's no point in displaying this activity.
+ if (K9.areDatabasesUpToDate()) {
+ launchOriginalActivity();
+ return;
+ }
+
+ mPreferences = Preferences.getPreferences(getApplicationContext());
+
+ initializeLayout();
+
+ decodeExtras();
+
+ setupBroadcastReceiver();
+ }
+
+ /**
+ * Initialize the activity's layout
+ */
+ private void initializeLayout() {
+ setContentView(R.layout.upgrade_databases);
+
+ mUpgradeText = (TextView) findViewById(R.id.databaseUpgradeText);
+ }
+
+ /**
+ * Decode extras in the intent used to start this activity.
+ */
+ private void decodeExtras() {
+ Intent intent = getIntent();
+ mStartIntent = intent.getParcelableExtra(EXTRA_START_INTENT);
+ }
+
+ /**
+ * Setup the broadcast receiver used to receive progress updates from
+ * {@link DatabaseUpgradeService}.
+ */
+ private void setupBroadcastReceiver() {
+ mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
+ mBroadcastReceiver = new UpgradeDatabaseBroadcastReceiver();
+
+ mIntentFilter = new IntentFilter(DatabaseUpgradeService.ACTION_UPGRADE_PROGRESS);
+ mIntentFilter.addAction(DatabaseUpgradeService.ACTION_UPGRADE_COMPLETE);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Check if the upgrade was completed while the activity was paused.
+ if (K9.areDatabasesUpToDate()) {
+ launchOriginalActivity();
+ return;
+ }
+
+ // Register the broadcast receiver to listen for progress reports from
+ // DatabaseUpgradeService.
+ mLocalBroadcastManager.registerReceiver(mBroadcastReceiver, mIntentFilter);
+
+ // Now that the broadcast receiver was registered start DatabaseUpgradeService.
+ DatabaseUpgradeService.startService(this);
+ }
+
+ @Override
+ public void onPause() {
+ // The activity is being paused, so there's no point in listening to the progress of the
+ // database upgrade service.
+ mLocalBroadcastManager.unregisterReceiver(mBroadcastReceiver);
+
+ super.onPause();
+ }
+
+ /**
+ * Finish this activity and launch the original activity using the supplied intent.
+ */
+ private void launchOriginalActivity() {
+ finish();
+ startActivity(mStartIntent);
+ }
+
+ /**
+ * Receiver for broadcasts send by {@link DatabaseUpgradeService}.
+ */
+ class UpgradeDatabaseBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, final Intent intent) {
+ String action = intent.getAction();
+
+ if (DatabaseUpgradeService.ACTION_UPGRADE_PROGRESS.equals(action)) {
+ /*
+ * Information on the current upgrade progress
+ */
+
+ String accountUuid = intent.getStringExtra(
+ DatabaseUpgradeService.EXTRA_ACCOUNT_UUID);
+
+ Account account = mPreferences.getAccount(accountUuid);
+
+ if (account != null) {
+ String formatString = getString(R.string.upgrade_database_format);
+ String upgradeStatus = String.format(formatString, account.getDescription());
+ mUpgradeText.setText(upgradeStatus);
+ }
+
+ } else if (DatabaseUpgradeService.ACTION_UPGRADE_COMPLETE.equals(action)) {
+ /*
+ * Upgrade complete
+ */
+
+ launchOriginalActivity();
+ }
+ }
+ }
+}
diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java
index b41a6a0cf..d3a67989d 100644
--- a/src/com/fsck/k9/mail/Store.java
+++ b/src/com/fsck/k9/mail/Store.java
@@ -3,6 +3,7 @@ package com.fsck.k9.mail;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import android.app.Application;
import android.content.Context;
@@ -33,10 +34,16 @@ public abstract class Store {
private static HashMap sStores = new HashMap();
/**
- * Local stores indexed by UUid because the Uri may change due to migration to/from SD-card.
+ * Local stores indexed by UUID because the Uri may change due to migration to/from SD-card.
*/
- private static HashMap sLocalStores = new HashMap();
+ private static ConcurrentHashMap sLocalStores = new ConcurrentHashMap();
+ /**
+ * Lock objects indexed by account UUID.
+ *
+ * @see #getLocalInstance(Account, Application)
+ */
+ private static ConcurrentHashMap sAccountLocks = new ConcurrentHashMap();
/**
* Get an instance of a remote mail store.
@@ -72,16 +79,36 @@ public abstract class Store {
/**
* Get an instance of a local mail store.
- * @throws UnavailableStorageException if not {@link StorageProvider#isReady(Context)}
+ *
+ * @throws UnavailableStorageException
+ * if not {@link StorageProvider#isReady(Context)}
*/
- public synchronized static LocalStore getLocalInstance(Account account, Application application) throws MessagingException {
- Store store = sLocalStores.get(account.getUuid());
- if (store == null) {
- store = new LocalStore(account, application);
- sLocalStores.put(account.getUuid(), store);
- }
+ public static LocalStore getLocalInstance(Account account, Application application)
+ throws MessagingException {
- return (LocalStore) store;
+ String accountUuid = account.getUuid();
+
+ // Create new per-account lock object if necessary
+ sAccountLocks.putIfAbsent(accountUuid, new Object());
+
+ // Get the account's lock object
+ Object lock = sAccountLocks.get(accountUuid);
+
+ // Use per-account locks so DatabaseUpgradeService always knows which account database is
+ // currently upgraded.
+ synchronized (lock) {
+ Store store = sLocalStores.get(accountUuid);
+
+ if (store == null) {
+ // Creating a LocalStore instance will create or upgrade the database if
+ // necessary. This could take some time.
+ store = new LocalStore(account, application);
+
+ sLocalStores.put(accountUuid, store);
+ }
+
+ return (LocalStore) store;
+ }
}
/**
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index af6548915..f31523164 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -111,7 +111,7 @@ public class LocalStore extends Store implements Serializable {
*/
private static final int UID_CHECK_BATCH_SIZE = 500;
- protected static final int DB_VERSION = 45;
+ public static final int DB_VERSION = 45;
protected String uUid = null;