1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-27 11:42:16 -05:00

Add activity that is displayed during database upgrades

See UpgradeDatabases.java for a detailed description of the upgrade
process.
This commit is contained in:
cketti 2012-12-01 08:02:55 +01:00
parent f67d543510
commit b2098c8d1c
8 changed files with 392 additions and 11 deletions

View File

@ -421,5 +421,13 @@ otherwise it would make K-9 start at the wrong time
<activity <activity
android:name=".activity.AccountList"> android:name=".activity.AccountList">
</activity> </activity>
<activity
android:name=".activity.UpgradeDatabases"
android:label="@string/upgrade_databases_title">
</activity>
<service
android:name=".service.DatabaseUpgradeService"
android:exported="false">
</service>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical" >
<ProgressBar
android:id="@+id/databaseUpgradeProgress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/databaseUpgradeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/upgrade_databases_unspecified" />
</LinearLayout>

View File

@ -1093,4 +1093,8 @@ http://k9mail.googlecode.com/
<string name="global_settings_threaded_view_label">Threaded view</string> <string name="global_settings_threaded_view_label">Threaded view</string>
<string name="global_settings_threaded_view_summary">Group messages by conversation</string> <string name="global_settings_threaded_view_summary">Group messages by conversation</string>
<string name="upgrade_databases_title">Upgrading databases</string>
<string name="upgrade_databases_unspecified">Upgrading databases…</string>
<string name="upgrade_database_format">Upgrading database of account \"<xliff:g id="account">%s</xliff:g>\"</string>
</resources> </resources>

View File

@ -15,6 +15,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
@ -27,12 +28,14 @@ import android.util.Log;
import com.fsck.k9.Account.SortType; import com.fsck.k9.Account.SortType;
import com.fsck.k9.activity.MessageCompose; import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.activity.UpgradeDatabases;
import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.provider.UnreadWidgetProvider; import com.fsck.k9.provider.UnreadWidgetProvider;
import com.fsck.k9.service.BootReceiver; import com.fsck.k9.service.BootReceiver;
import com.fsck.k9.service.MailService; import com.fsck.k9.service.MailService;
@ -64,6 +67,23 @@ public class K9 extends Application {
public static File tempDirectory; public static File tempDirectory;
public static final String LOG_TAG = "k9"; public static final String LOG_TAG = "k9";
/**
* Name of the {@link SharedPreferences} file used to store the last known version of the
* accounts' databases.
*
* <p>
* See {@link UpgradeDatabases} for a detailed explanation of the database upgrade process.
* </p>
*/
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 * Components that are interested in knowing when the K9 instance is
* available and ready. * available and ready.
@ -150,6 +170,16 @@ public class K9 extends Application {
public static boolean ENABLE_ERROR_FOLDER = true; public static boolean ENABLE_ERROR_FOLDER = true;
public static String ERROR_FOLDER_NAME = "K9mail-errors"; 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 mAnimations = true;
private static boolean mConfirmDelete = false; private static boolean mConfirmDelete = false;
@ -208,6 +238,11 @@ public class K9 extends Application {
private static boolean sUseBackgroundAsUnreadIndicator = true; private static boolean sUseBackgroundAsUnreadIndicator = true;
private static boolean sThreadedViewEnabled = true; private static boolean sThreadedViewEnabled = true;
/**
* @see #areDatabasesUpToDate()
*/
private static boolean sDatabasesUpToDate = false;
/** /**
* The MIME type(s) of attachments we're willing to view. * The MIME type(s) of attachments we're willing to view.
@ -487,6 +522,8 @@ public class K9 extends Application {
galleryBuggy = checkForBuggyGallery(); galleryBuggy = checkForBuggyGallery();
checkCachedDatabaseVersion();
Preferences prefs = Preferences.getPreferences(this); Preferences prefs = Preferences.getPreferences(this);
loadPrefs(prefs); loadPrefs(prefs);
@ -581,6 +618,31 @@ public class K9 extends Application {
notifyObservers(); notifyObservers();
} }
/**
* Loads the last known database version of the accounts' databases from a
* {@link SharedPreference}.
*
* <p>
* If the stored version matches {@link LocalStore#DB_VERSION} we know that the databases are
* up to date.<br>
* Using {@code SharedPreferences} should be a lot faster than opening all SQLite databases to
* get the current database version.
* </p><p>
* See {@link UpgradeDatabases} for a detailed explanation of the database upgrade process.
* </p>
*
* @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) { public static void loadPrefs(Preferences prefs) {
SharedPreferences sprefs = prefs.getPreferences(); SharedPreferences sprefs = prefs.getPreferences();
DEBUG = sprefs.getBoolean("enableDebugLogging", false); DEBUG = sprefs.getBoolean("enableDebugLogging", false);
@ -1150,4 +1212,38 @@ public class K9 extends Application {
public static synchronized void setThreadedViewEnabled(boolean enable) { public static synchronized void setThreadedViewEnabled(boolean enable) {
sThreadedViewEnabled = enable; sThreadedViewEnabled = enable;
} }
/**
* Check if we already know whether all databases are using the current database schema.
*
* <p>
* 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.
* </p>
*
* @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();
}
}
} }

View File

@ -371,6 +371,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
if (accounts.length < 1) { if (accounts.length < 1) {
WelcomeMessage.showWelcomeMessage(this); WelcomeMessage.showWelcomeMessage(this);
finish(); finish();
return;
}
if (UpgradeDatabases.actionUpgradeDatabases(this, intent)) {
finish();
return;
} }
boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true); boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true);

View File

@ -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.
*
* <p>
* The current upgrade process works as follows:
* <ol>
* <li>Activities that access an account's database call
* {@link #actionUpgradeDatabases(Context, Intent)} in their {@link Activity#onCreate(Bundle)}
* method.</li>
* <li>{@link #actionUpgradeDatabases(Context, Intent)} will call {@link K9#areDatabasesUpToDate()}
* to check if we already know whether the databases have been upgraded.</li>
* <li>{@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.</li>
* <li>If there was an error reading the cached database version or if it shows the databases need
* upgrading this activity ({@code UpgradeDatabases}) is started.</li>
* <li>This activity will display a spinning progress indicator and start
* {@link DatabaseUpgradeService}.</li>
* <li>{@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.</li>
* <li>Once the upgrade is complete {@link DatabaseUpgradeService} will notify this activity,
* release the wake lock, and stop itself.</li>
* <li>This activity will start the original activity using the intent supplied when calling
* {@link #actionUpgradeDatabases(Context, Intent)}.</li>
* </ol>
* </p><p>
* 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.
* </p>
*/
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.<br>
* {@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();
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.fsck.k9.mail;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
@ -33,10 +34,16 @@ public abstract class Store {
private static HashMap<String, Store> sStores = new HashMap<String, Store>(); private static HashMap<String, Store> sStores = new HashMap<String, Store>();
/** /**
* 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<String, Store> sLocalStores = new HashMap<String, Store>(); private static ConcurrentHashMap<String, Store> sLocalStores = new ConcurrentHashMap<String, Store>();
/**
* Lock objects indexed by account UUID.
*
* @see #getLocalInstance(Account, Application)
*/
private static ConcurrentHashMap<String, Object> sAccountLocks = new ConcurrentHashMap<String, Object>();
/** /**
* Get an instance of a remote mail store. * Get an instance of a remote mail store.
@ -72,16 +79,36 @@ public abstract class Store {
/** /**
* Get an instance of a local mail 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 { public static LocalStore getLocalInstance(Account account, Application application)
Store store = sLocalStores.get(account.getUuid()); throws MessagingException {
if (store == null) {
store = new LocalStore(account, application);
sLocalStores.put(account.getUuid(), store);
}
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;
}
} }
/** /**

View File

@ -111,7 +111,7 @@ public class LocalStore extends Store implements Serializable {
*/ */
private static final int UID_CHECK_BATCH_SIZE = 500; 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; protected String uUid = null;