Quick backup/restore of accounts

* Workaround for (HTC bug 2275383) & (Moto bug 2226582)
* Adds checkpoints for backing up and restoring accounts
* Uses legacy Account / prefs to back up accounts - this is because
  some of this code will be reused for legacy account migration
* Unit tests of Account & LegacyConversions
* Unit tests of backup & restore
* Not done:  testing of EAS/Account Manager interface (this will require
  deeper dependency injection, to avoid the embedded calls to the Account
  Manager and other system services.)
This commit is contained in:
Andrew Stadler 2010-01-20 01:36:01 -08:00
parent 950f6c65dd
commit 5e91cccd4b
20 changed files with 861 additions and 19 deletions

View File

@ -22,7 +22,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import java.io.Serializable;
import java.util.Arrays;
import java.util.UUID;
@ -30,7 +29,7 @@ import java.util.UUID;
* Account stores all of the settings for a single account defined by the user. It is able to save
* and delete itself given a Preferences to work with. Each account is defined by a UUID.
*/
public class Account implements Serializable {
public class Account {
public static final int DELETE_POLICY_NEVER = 0;
public static final int DELETE_POLICY_7DAYS = 1;
public static final int DELETE_POLICY_ON_DELETE = 2;
@ -46,12 +45,11 @@ public class Account implements Serializable {
public static final int SYNC_WINDOW_1_MONTH = 5;
public static final int SYNC_WINDOW_ALL = 6;
/**
* This should never be used for persistance, only for marshalling.
* TODO: Remove serializable (VERY SLOW) and replace with Parcelable
*/
private static final long serialVersionUID = 1L;
// These flags will never be seen in a "real" (legacy) account
public static final int BACKUP_FLAGS_IS_BACKUP = 1;
public static final int BACKUP_FLAGS_SYNC_CONTACTS = 2;
public static final int BACKUP_FLAGS_IS_DEFAULT = 4;
// transient values - do not serialize
private transient Preferences mPreferences;
@ -74,6 +72,8 @@ public class Account implements Serializable {
boolean mVibrate;
String mRingtoneUri;
int mSyncWindow;
int mBackupFlags; // for account backups only
String mProtocolVersion; // for account backups only
/**
* <pre>
@ -88,6 +88,8 @@ public class Account implements Serializable {
* All new fields should have named keys
*/
private final String KEY_SYNC_WINDOW = ".syncWindow";
private final String KEY_BACKUP_FLAGS = ".backupFlags";
private final String KEY_PROTOCOL_VERSION = ".protocolVersion";
public Account(Context context) {
// TODO Change local store path to something readable / recognizable
@ -99,6 +101,8 @@ public class Account implements Serializable {
mVibrate = false;
mRingtoneUri = "content://settings/system/notification_sound";
mSyncWindow = SYNC_WINDOW_USER; // IMAP & POP3
mBackupFlags = 0;
mProtocolVersion = null;
}
Account(Preferences preferences, String uuid) {
@ -157,6 +161,10 @@ public class Account implements Serializable {
mSyncWindow = preferences.mSharedPreferences.getInt(mUuid + KEY_SYNC_WINDOW,
SYNC_WINDOW_USER);
mBackupFlags = preferences.mSharedPreferences.getInt(mUuid + KEY_BACKUP_FLAGS, 0);
mProtocolVersion = preferences.mSharedPreferences.getString(mUuid + KEY_PROTOCOL_VERSION,
null);
}
public String getUuid() {
@ -252,6 +260,8 @@ public class Account implements Serializable {
editor.remove(mUuid + ".vibrate");
editor.remove(mUuid + ".ringtone");
editor.remove(mUuid + KEY_SYNC_WINDOW);
editor.remove(mUuid + KEY_BACKUP_FLAGS);
editor.remove(mUuid + KEY_PROTOCOL_VERSION);
// also delete any deprecated fields
editor.remove(mUuid + ".transportUri");
@ -315,6 +325,8 @@ public class Account implements Serializable {
editor.putBoolean(mUuid + ".vibrate", mVibrate);
editor.putString(mUuid + ".ringtone", mRingtoneUri);
editor.putInt(mUuid + KEY_SYNC_WINDOW, mSyncWindow);
editor.putInt(mUuid + KEY_BACKUP_FLAGS, mBackupFlags);
editor.putString(mUuid + KEY_PROTOCOL_VERSION, mProtocolVersion);
// The following fields are *not* written because they need to be more fine-grained
// and not risk rewriting with old data.

View File

@ -0,0 +1,224 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email;
import com.android.email.mail.store.ExchangeStore;
import com.android.email.provider.EmailContent;
import com.android.exchange.Eas;
import android.accounts.AccountManagerFuture;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.util.Log;
/**
* Utility functions to support backup and restore of accounts.
*
* In the short term, this is used to work around local database failures. In the long term,
* this will also support server-side backups, providing support for automatic account restoration
* when switching or replacing phones.
*/
public class AccountBackupRestore {
/**
* Backup accounts. Can be called from UI thread (does work in a new thread)
*/
public static void backupAccounts(final Context context) {
if (Email.DEBUG) {
Log.v(Email.LOG_TAG, "backupAccounts");
}
// Because we typically call this from the UI, let's do the work in a thread
new Thread() {
@Override
public void run() {
synchronized(AccountBackupRestore.class) {
doBackupAccounts(context, Preferences.getPreferences(context));
}
}
}.start();
}
/**
* Restore accounts if needed. This is blocking, and should only be called in specific
* startup/entry points.
*/
public static void restoreAccountsIfNeeded(final Context context) {
// Don't log here; This is called often.
boolean restored;
synchronized(AccountBackupRestore.class) {
restored = doRestoreAccounts(context, Preferences.getPreferences(context));
}
if (restored) {
// after restoring accounts, register services appropriately
Log.w(Email.LOG_TAG, "Register services after restoring accounts");
Email.setServicesEnabled(context);
context.startService(new Intent(context.getApplicationContext(),
com.android.exchange.SyncManager.class));
}
}
/**
* Non-UI-Thread worker to backup all accounts
*
* @param context used to access the provider
* @param preferences used to access the backups (provided separately for testability)
*/
/* package */ static void doBackupAccounts(Context context, Preferences preferences) {
// 1. Wipe any existing backup accounts
Account[] oldBackups = preferences.getAccounts();
for (Account backup : oldBackups) {
backup.delete(preferences);
}
// 2. Identify the default account (if any). This is required because setting
// the default account flag is lazy,and sometimes we don't have any flags set. We'll
// use this to make it explicit (see loop, below).
long defaultAccountId = EmailContent.Account.getDefaultAccountId(context);
if (defaultAccountId == -1) {
return;
}
// 3. Create new backup(s), if any
Cursor c = context.getContentResolver().query(EmailContent.Account.CONTENT_URI,
EmailContent.Account.CONTENT_PROJECTION, null, null, null);
try {
while (c.moveToNext()) {
EmailContent.Account fromAccount =
EmailContent.getContent(c, EmailContent.Account.class);
if (Email.DEBUG) {
Log.v(Email.LOG_TAG, "Backing up account:" + fromAccount.getDisplayName());
}
Account toAccount = LegacyConversions.makeLegacyAccount(context, fromAccount);
// Determine if contacts are also synced, and if so, record that
if (fromAccount.mHostAuthRecv.mProtocol.equals("eas")) {
android.accounts.Account acct = new android.accounts.Account(
fromAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
boolean syncContacts = ContentResolver.getSyncAutomatically(acct,
ContactsContract.AUTHORITY);
if (syncContacts) {
toAccount.mBackupFlags |= Account.BACKUP_FLAGS_SYNC_CONTACTS;
}
}
// If this is the default account, mark it as such
if (fromAccount.mId == defaultAccountId) {
toAccount.mBackupFlags |= Account.BACKUP_FLAGS_IS_DEFAULT;
}
// Mark this account as a backup of a Provider account, instead of a legacy
// account to upgrade
toAccount.mBackupFlags |= Account.BACKUP_FLAGS_IS_BACKUP;
toAccount.save(preferences);
}
} finally {
c.close();
}
}
/**
* Restore all accounts. This is blocking.
*
* @param context used to access the provider
* @param preferences used to access the backups (provided separately for testability)
* @return true if accounts were restored (meaning services should be restarted, etc.)
*/
/* package */ static boolean doRestoreAccounts(Context context, Preferences preferences) {
boolean result = false;
// 1. Quick check - if we have any accounts, get out
int numAccounts = EmailContent.count(context, EmailContent.Account.CONTENT_URI, null, null);
if (numAccounts > 0) {
return result;
}
// 2. Quick check - if no backup accounts, get out
Account[] backups = preferences.getAccounts();
if (backups.length == 0) {
return result;
}
Log.w(Email.LOG_TAG, "*** Restoring Email Accounts, found " + backups.length);
// 3. Possible lost accounts situation - check for any backups, and restore them
for (Account backupAccount : backups) {
// don't back up any leftover legacy accounts (these are migrated elsewhere).
if ((backupAccount.mBackupFlags & Account.BACKUP_FLAGS_IS_BACKUP) == 0) {
continue;
}
// Restore the account
Log.v(Email.LOG_TAG, "Restoring account:" + backupAccount.getDescription());
EmailContent.Account toAccount =
LegacyConversions.makeAccount(context, backupAccount);
// Mark the default account if this is it
if (0 != (backupAccount.mBackupFlags & Account.BACKUP_FLAGS_IS_DEFAULT)) {
toAccount.setDefaultAccount(true);
}
// For exchange accounts, handle system account first, then save in provider
if (toAccount.mHostAuthRecv.mProtocol.equals("eas")) {
// Recreate entry in Account Manager as well, if needed
// Set "sync contacts" mode as well, if needed
boolean alsoSyncContacts =
(backupAccount.mBackupFlags & Account.BACKUP_FLAGS_SYNC_CONTACTS) != 0;
// Use delete-then-add semantic to simplify handling of update-in-place
// AccountManagerFuture<Boolean> removeResult = ExchangeStore.removeSystemAccount(
// context.getApplicationContext(), toAccount, null);
// try {
// // This call blocks until removeSystemAccount completes. Result is not used.
// removeResult.getResult();
// } catch (AccountsException e) {
// Log.d(Email.LOG_TAG, "removeSystemAccount failed: " + e);
// // log and discard - we don't care if remove fails, generally
// } catch (IOException e) {
// Log.d(Email.LOG_TAG, "removeSystemAccount failed: " + e);
// // log and discard - we don't care if remove fails, generally
// }
// NOTE: We must use the Application here, rather than the current context, because
// all future references to AccountManager will use the context passed in here
// TODO: Need to implement overwrite semantics for an already-installed account
AccountManagerFuture<Bundle> addAccountResult = ExchangeStore.addSystemAccount(
context.getApplicationContext(), toAccount, alsoSyncContacts, null);
// try {
// // This call blocks until addSystemAccount completes. Result is not used.
// addAccountResult.getResult();
toAccount.save(context);
// } catch (OperationCanceledException e) {
// Log.d(Email.LOG_TAG, "addAccount was canceled");
// } catch (IOException e) {
// Log.d(Email.LOG_TAG, "addAccount failed: " + e);
// } catch (AuthenticatorException e) {
// Log.d(Email.LOG_TAG, "addAccount failed: " + e);
// }
} else {
// non-eas account - save it immediately
toAccount.save(context);
}
// report that an account was restored
result = true;
}
return result;
}
}

View File

@ -521,4 +521,84 @@ public class LegacyConversions {
}
mp.addBodyPart(bp);
}
/**
* Conversion from provider account to legacy account
*
* Used for backup/restore.
*
* @param context application context
* @param fromAccount the provider account to be backed up (including transient hostauth's)
* @result a legacy Account object ready to be committed to preferences
*/
/* package */ static Account makeLegacyAccount(Context context,
EmailContent.Account fromAccount) {
Account result = new Account(context);
result.setDescription(fromAccount.getDisplayName());
result.setEmail(fromAccount.getEmailAddress());
// fromAccount.mSyncKey - assume lost if restoring
result.setSyncWindow(fromAccount.getSyncLookback());
result.setAutomaticCheckIntervalMinutes(fromAccount.getSyncInterval());
// fromAccount.mHostAuthKeyRecv - id not saved; will be reassigned when restoring
// fromAccount.mHostAuthKeySend - id not saved; will be reassigned when restoring
// Provider Account flags, and how they are mapped.
// FLAGS_NOTIFY_NEW_MAIL -> mNotifyNewMail
// FLAGS_VIBRATE -> mVibrate
// DELETE_POLICY_NEVER -> mDeletePolicy
// DELETE_POLICY_7DAYS
// DELETE_POLICY_ON_DELETE
result.setNotifyNewMail(0 !=
(fromAccount.getFlags() & EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL));
result.setVibrate(0 !=
(fromAccount.getFlags() & EmailContent.Account.FLAGS_VIBRATE));
result.setDeletePolicy(fromAccount.getDeletePolicy());
result.mUuid = fromAccount.getUuid();
result.setName(fromAccount.mSenderName);
result.setRingtone(fromAccount.mRingtoneUri);
result.mProtocolVersion = fromAccount.mProtocolVersion;
// int fromAccount.mNewMessageCount = will be reset on next sync
// Use the existing conversions from HostAuth <-> Uri
result.setStoreUri(fromAccount.getStoreUri(context));
result.setSenderUri(fromAccount.getSenderUri(context));
return result;
}
/**
* Conversion from legacy account to provider account
*
* Used for backup/restore and for account migration.
*/
/* package */ static EmailContent.Account makeAccount(Context context, Account fromAccount) {
EmailContent.Account result = new EmailContent.Account();
result.setDisplayName(fromAccount.getDescription());
result.setEmailAddress(fromAccount.getEmail());
result.mSyncKey = "";
result.setSyncLookback(fromAccount.getSyncWindow());
result.setSyncInterval(fromAccount.getAutomaticCheckIntervalMinutes());
// result.mHostAuthKeyRecv
// result.mHostAuthKeySend;
int flags = 0;
if (fromAccount.isNotifyNewMail()) flags |= EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL;
if (fromAccount.isVibrate()) flags |= EmailContent.Account.FLAGS_VIBRATE;
result.setFlags(flags);
result.setDeletePolicy(fromAccount.getDeletePolicy());
// result.setDefaultAccount();
result.mCompatibilityUuid = fromAccount.getUuid();
result.setSenderName(fromAccount.getName());
result.setRingtone(fromAccount.getRingtone());
result.mProtocolVersion = fromAccount.mProtocolVersion;
result.mNewMessageCount = 0;
result.setStoreUri(context, fromAccount.getStoreUri());
result.setSenderUri(context, fromAccount.getSenderUri());
return result;
}
}

View File

@ -16,6 +16,7 @@
package com.android.email.activity;
import com.android.email.AccountBackupRestore;
import com.android.email.Controller;
import com.android.email.Email;
import com.android.email.R;
@ -447,6 +448,8 @@ public class AccountFolderList extends ListActivity implements OnItemClickListen
Uri uri = ContentUris.withAppendedId(
EmailContent.Account.CONTENT_URI, mSelectedContextAccount.mId);
AccountFolderList.this.getContentResolver().delete(uri, null, null);
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(AccountFolderList.this);
} catch (Exception e) {
// Ignore
}

View File

@ -16,6 +16,7 @@
package com.android.email.activity;
import com.android.email.AccountBackupRestore;
import com.android.email.activity.setup.AccountSetupBasics;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Mailbox;
@ -47,6 +48,13 @@ public class Welcome extends Activity {
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
// Restore accounts, if it has not happened already
// NOTE: This is blocking, which it should not be (in the UI thread)
// We're going to live with this for the short term and replace with something
// smarter. Long-term fix: Move this, and most of the code below, to an AsyncTask
// and do the DB work in a thread. Then post handler to finish() as appropriate.
AccountBackupRestore.restoreAccountsIfNeeded(this);
// Because the app could be reloaded (for debugging, etc.), we need to make sure that
// SyncManager gets a chance to start. There is no harm to starting it if it has already
// been started

View File

@ -16,6 +16,7 @@
package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.AccountColumns;
@ -45,5 +46,7 @@ public class AccountSettingsUtils {
cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
account.update(context, cv);
}
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(context);
}
}

View File

@ -16,6 +16,7 @@
package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore;
import com.android.email.Email;
import com.android.email.EmailAddressValidator;
import com.android.email.R;
@ -444,6 +445,8 @@ public class AccountSetupBasics extends Activity
// At this point we write the Account object to the DB for the first time.
// From now on we'll only pass the accountId around.
mAccount.save(this);
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(this);
Email.setServicesEnabled(this);
AccountSetupNames.actionSetNames(this, mAccount.mId, false);
finish();

View File

@ -16,6 +16,7 @@
package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore;
import com.android.email.R;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
@ -310,6 +311,8 @@ public class AccountSetupExchange extends Activity implements OnClickListener,
// Account.save will save the HostAuth's
mAccount.save(this);
}
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(this);
finish();
} else {
// Go directly to end - there is no 2nd screen for incoming settings

View File

@ -17,6 +17,7 @@
package com.android.email.activity.setup;
import com.android.email.Account;
import com.android.email.AccountBackupRestore;
import com.android.email.R;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
@ -334,6 +335,8 @@ public class AccountSetupIncoming extends Activity implements OnClickListener {
} else {
mAccount.save(this);
}
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(this);
finish();
} else {
/*

View File

@ -16,6 +16,7 @@
package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore;
import com.android.email.R;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
@ -127,6 +128,8 @@ public class AccountSetupNames extends Activity implements OnClickListener {
cv.put(AccountColumns.DISPLAY_NAME, mAccount.getDisplayName());
cv.put(AccountColumns.SENDER_NAME, name);
mAccount.update(this, cv);
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(this);
onBackPressed();
}

View File

@ -16,6 +16,7 @@
package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore;
import com.android.email.R;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
@ -255,6 +256,8 @@ public class AccountSetupOutgoing extends Activity implements OnClickListener,
} else {
mAccount.save(this);
}
// Update the backup (side copy) of the accounts
AccountBackupRestore.backupAccounts(this);
finish();
} else {
AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault, false);

View File

@ -29,6 +29,7 @@ import com.android.exchange.SyncManager;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
@ -79,8 +80,8 @@ public class ExchangeStore extends Store {
mTransport.checkSettings(mUri);
}
static public void addSystemAccount(Context context, Account acct, boolean syncContacts,
AccountManagerCallback<Bundle> callback) {
static public AccountManagerFuture<Bundle> addSystemAccount(Context context, Account acct,
boolean syncContacts, AccountManagerCallback<Bundle> callback) {
// Create a description of the new account
Bundle options = new Bundle();
options.putString(EasAuthenticatorService.OPTIONS_USERNAME, acct.mEmailAddress);
@ -90,10 +91,25 @@ public class ExchangeStore extends Store {
// Here's where we tell AccountManager about the new account. The addAccount
// method in AccountManager calls the addAccount method in our authenticator
// service (EasAuthenticatorService)
AccountManager.get(context).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null,
return AccountManager.get(context).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null,
options, null, callback, null);
}
/**
* Remove an account from the Account manager - see {@link AccountManager#removeAccount(
* android.accounts.Account, AccountManagerCallback, android.os.Handler)}.
*
* @param context context to use
* @param acct the account to remove
* @param callback async results callback - pass null to use blocking mode
*/
static public AccountManagerFuture<Boolean> removeSystemAccount(Context context, Account acct,
AccountManagerCallback<Bundle> callback) {
android.accounts.Account systemAccount =
new android.accounts.Account(acct.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
return AccountManager.get(context).removeAccount(systemAccount, null, null);
}
@Override
public Folder getFolder(String name) {
return null;

View File

@ -1067,7 +1067,7 @@ public abstract class EmailContent {
* with accounts set up by previous versions, because there are externals references
* to the Uuid (e.g. desktop shortcuts).
*/
String getUuid() {
public String getUuid() {
return mCompatibilityUuid;
}

View File

@ -16,6 +16,7 @@
package com.android.email.service;
import com.android.email.AccountBackupRestore;
import com.android.email.Email;
import android.content.BroadcastReceiver;
@ -25,6 +26,9 @@ import android.content.Intent;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(context);
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
// Returns true if there are any accounts
if (Email.setServicesEnabled(context)) {

View File

@ -16,6 +16,7 @@
package com.android.email.service;
import com.android.email.AccountBackupRestore;
import com.android.email.Controller;
import com.android.email.Email;
import com.android.email.R;
@ -149,6 +150,9 @@ public class MailService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
// Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(this);
// TODO this needs to be passed through the controller and back to us
this.mStartId = startId;
String action = intent.getAction();

View File

@ -17,6 +17,7 @@
package com.android.exchange;
import com.android.email.AccountBackupRestore;
import com.android.email.mail.MessagingException;
import com.android.email.mail.store.TrustManagerFactory;
import com.android.email.provider.EmailContent;
@ -826,6 +827,10 @@ public class SyncManager extends Service implements Runnable {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
alwaysLog("!!! EAS SyncManager, onStartCommand");
// Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(this);
maybeStartSyncManagerThread();
if (sServiceThread == null) {
alwaysLog("!!! EAS SyncManager, stopping self");

View File

@ -0,0 +1,311 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
import android.content.Context;
import android.database.Cursor;
import android.test.ProviderTestCase2;
import android.test.suitebuilder.annotation.MediumTest;
/**
* This is a series of unit tests for backup/restore of the Account class.
*
* Technically these are functional because they use the underlying preferences framework.
*
* NOTE: These tests are destructive of any "legacy" accounts that might be lying around.
*/
@MediumTest
public class AccountBackupRestoreTests extends ProviderTestCase2<EmailProvider> {
private Preferences mPreferences;
private Context mMockContext;
public AccountBackupRestoreTests() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mMockContext = getMockContext();
// Note: preferences are not supported by this mock context, so we must
// explicitly use (and clean out) the real ones for now.
mPreferences = Preferences.getPreferences(mContext);
}
/**
* Delete any dummy accounts we set up for this test
*/
@Override
protected void tearDown() throws Exception {
super.tearDown();
deleteLegacyAccounts();
}
/**
* Delete *all* legacy accounts
*/
private void deleteLegacyAccounts() {
Account[] oldAccounts = mPreferences.getAccounts();
for (Account oldAccount : oldAccounts) {
oldAccount.delete(mPreferences);
}
}
/**
* Test backup with no accounts
*/
public void testNoAccountBackup() {
// create some "old" backups or legacy accounts
Account backupAccount = new Account(mMockContext);
backupAccount.save(mPreferences);
// confirm they are there
Account[] oldBackups = mPreferences.getAccounts();
assertTrue(oldBackups.length >= 1);
// make sure there are no accounts in the provider
int numAccounts = EmailContent.count(mMockContext, EmailContent.Account.CONTENT_URI,
null, null);
assertEquals(0, numAccounts);
// run backups
AccountBackupRestore.doBackupAccounts(mMockContext, mPreferences);
// confirm there are no backups made
Account[] backups = mPreferences.getAccounts();
assertEquals(0, backups.length);
}
/**
* Test backup with accounts
*/
public void testBackup() {
// Clear the decks
deleteLegacyAccounts();
// Create real accounts in need of backup
EmailContent.Account liveAccount1 =
ProviderTestUtils.setupAccount("testBackup1", false, mMockContext);
liveAccount1.mHostAuthRecv =
ProviderTestUtils.setupHostAuth("legacy-recv", 0, false, mMockContext);
liveAccount1.mHostAuthSend =
ProviderTestUtils.setupHostAuth("legacy-send", 0, false, mMockContext);
liveAccount1.setDefaultAccount(true);
liveAccount1.save(mMockContext);
EmailContent.Account liveAccount2 =
ProviderTestUtils.setupAccount("testBackup2", false, mMockContext);
liveAccount2.mHostAuthRecv =
ProviderTestUtils.setupHostAuth("legacy-recv", 0, false, mMockContext);
liveAccount2.mHostAuthSend =
ProviderTestUtils.setupHostAuth("legacy-send", 0, false, mMockContext);
liveAccount2.setDefaultAccount(false);
liveAccount2.save(mMockContext);
// run backups
AccountBackupRestore.doBackupAccounts(mMockContext, mPreferences);
// Confirm we have two backups now
// Deep inspection is not performed here - see LegacyConversionsTests
// We just check for basic identity & flags
Account[] backups = mPreferences.getAccounts();
assertEquals(2, backups.length);
for (Account backup : backups) {
if ("testBackup1".equals(backup.getDescription())) {
assertTrue(0 != (backup.mBackupFlags & Account.BACKUP_FLAGS_IS_DEFAULT));
} else if ("testBackup2".equals(backup.getDescription())) {
assertFalse(0 != (backup.mBackupFlags & Account.BACKUP_FLAGS_IS_DEFAULT));
} else {
fail("unexpected backup name=" + backup.getDescription());
}
}
Account backup1 = backups[0];
assertTrue(0 != (backup1.mBackupFlags & Account.BACKUP_FLAGS_IS_BACKUP));
assertEquals(liveAccount1.getDisplayName(), backup1.getDescription());
}
/**
* TODO: Test backup EAS accounts, with and without contacts sync
*
* Blocker: We need to inject the dependency on ContentResolver.getSyncAutomatically()
* so we can make our fake accounts appear to be syncable or non-syncable
*/
/**
* Test no-restore with accounts found
*/
public void testNoAccountRestore1() {
// make sure there are no real backups
deleteLegacyAccounts();
// make sure there are test backups available
Account backupAccount1 = setupLegacyBackupAccount("backup1");
backupAccount1.save(mPreferences);
Account backupAccount2 = setupLegacyBackupAccount("backup2");
backupAccount2.save(mPreferences);
// make sure there are accounts
EmailContent.Account existing =
ProviderTestUtils.setupAccount("existing", true, mMockContext);
// run the restore
boolean anyRestored = AccountBackupRestore.doRestoreAccounts(mMockContext, mPreferences);
assertFalse(anyRestored);
// make sure accounts still there
int numAccounts = EmailContent.count(mMockContext, EmailContent.Account.CONTENT_URI,
null, null);
assertEquals(1, numAccounts);
}
/**
* Test no-restore with no accounts & no backups
*/
public void testNoAccountRestore2() {
// make sure there are no real backups
deleteLegacyAccounts();
// make sure there are no accounts
int numAccounts = EmailContent.count(mMockContext, EmailContent.Account.CONTENT_URI,
null, null);
assertEquals(0, numAccounts);
// run the restore
boolean anyRestored = AccountBackupRestore.doRestoreAccounts(mMockContext, mPreferences);
assertFalse(anyRestored);
// make sure accounts still there
numAccounts = EmailContent.count(mMockContext, EmailContent.Account.CONTENT_URI,
null, null);
assertEquals(0, numAccounts);
}
/**
* Test restore with 2 accounts.
* Repeats test to verify restore of default account
*/
public void testAccountRestore() {
// make sure there are no real backups
deleteLegacyAccounts();
// create test backups
Account backupAccount1 = setupLegacyBackupAccount("backup1");
backupAccount1.mBackupFlags |= Account.BACKUP_FLAGS_IS_DEFAULT;
backupAccount1.save(mPreferences);
Account backupAccount2 = setupLegacyBackupAccount("backup2");
backupAccount2.save(mPreferences);
// run the restore
boolean anyRestored = AccountBackupRestore.doRestoreAccounts(mMockContext, mPreferences);
assertTrue(anyRestored);
// Check the restored accounts
// Deep inspection is not performed here - see LegacyConversionsTests for that
// We just check for basic identity & flags
Cursor c = mMockContext.getContentResolver().query(EmailContent.Account.CONTENT_URI,
EmailContent.Account.CONTENT_PROJECTION, null, null, null);
try {
assertEquals(2, c.getCount());
while (c.moveToNext()) {
EmailContent.Account restored =
EmailContent.getContent(c, EmailContent.Account.class);
if ("backup1".equals(restored.getDisplayName())) {
assertTrue(restored.mIsDefault);
} else if ("backup2".equals(restored.getDisplayName())) {
assertFalse(restored.mIsDefault);
} else {
fail("Unexpected restore account name=" + restored.getDisplayName());
}
}
} finally {
c.close();
}
// clear out the backups & accounts and try again
deleteLegacyAccounts();
mMockContext.getContentResolver().delete(EmailContent.Account.CONTENT_URI, null, null);
Account backupAccount3 = setupLegacyBackupAccount("backup3");
backupAccount3.save(mPreferences);
Account backupAccount4 = setupLegacyBackupAccount("backup4");
backupAccount4.mBackupFlags |= Account.BACKUP_FLAGS_IS_DEFAULT;
backupAccount4.save(mPreferences);
// run the restore
AccountBackupRestore.doRestoreAccounts(mMockContext, mPreferences);
// Check the restored accounts
// Deep inspection is not performed here - see LegacyConversionsTests for that
// We just check for basic identity & flags
c = mMockContext.getContentResolver().query(EmailContent.Account.CONTENT_URI,
EmailContent.Account.CONTENT_PROJECTION, null, null, null);
try {
assertEquals(2, c.getCount());
while (c.moveToNext()) {
EmailContent.Account restored =
EmailContent.getContent(c, EmailContent.Account.class);
if ("backup3".equals(restored.getDisplayName())) {
assertFalse(restored.mIsDefault);
} else if ("backup4".equals(restored.getDisplayName())) {
assertTrue(restored.mIsDefault);
} else {
fail("Unexpected restore account name=" + restored.getDisplayName());
}
}
} finally {
c.close();
}
}
/**
* TODO: Test restore EAS accounts, with and without contacts sync
*
* Blocker: We need to inject the dependency on account manager to catch the calls to it
*/
/**
* Setup a legacy backup account with many fields prefilled.
*/
private Account setupLegacyBackupAccount(String name) {
Account backup = new Account(mMockContext);
// fill in useful fields
backup.mUuid = "test-uid-" + name;
backup.mStoreUri = "store://test/" + name;
backup.mLocalStoreUri = "local://localhost/" + name;
backup.mSenderUri = "sender://test/" + name;
backup.mDescription = name;
backup.mName = "name " + name;
backup.mEmail = "email " + name;
backup.mAutomaticCheckIntervalMinutes = 100;
backup.mLastAutomaticCheckTime = 200;
backup.mNotifyNewMail = true;
backup.mDraftsFolderName = "drafts " + name;
backup.mSentFolderName = "sent " + name;
backup.mTrashFolderName = "trash " + name;
backup.mOutboxFolderName = "outbox " + name;
backup.mAccountNumber = 300;
backup.mVibrate = true;
backup.mRingtoneUri = "ringtone://test/" + name;
backup.mSyncWindow = 400;
backup.mBackupFlags = Account.BACKUP_FLAGS_IS_BACKUP;
backup.mProtocolVersion = "proto version" + name;
backup.mDeletePolicy = Account.DELETE_POLICY_NEVER;
return backup;
}
}

View File

@ -134,7 +134,37 @@ public class AccountUnitTests extends AndroidTestCase {
storedPolicy = mPreferences.mSharedPreferences.getInt(mUuid + ".deletePolicy", -1);
assertEquals(Account.DELETE_POLICY_ON_DELETE, storedPolicy);
}
/**
* Test new flags field (added only for backups - not used by real/legacy accounts)
*/
public void testFlagsField() {
createTestAccount();
assertEquals(0, mAccount.mBackupFlags);
mAccount.save(mPreferences);
mAccount.mBackupFlags = -1;
mAccount.refresh(mPreferences);
assertEquals(0, mAccount.mBackupFlags);
mAccount.mBackupFlags = Account.BACKUP_FLAGS_IS_BACKUP;
mAccount.save(mPreferences);
mAccount.mBackupFlags = -1;
mAccount.refresh(mPreferences);
assertEquals(Account.BACKUP_FLAGS_IS_BACKUP, mAccount.mBackupFlags);
mAccount.mBackupFlags = Account.BACKUP_FLAGS_SYNC_CONTACTS;
mAccount.save(mPreferences);
mAccount.mBackupFlags = -1;
mAccount.refresh(mPreferences);
assertEquals(Account.BACKUP_FLAGS_SYNC_CONTACTS, mAccount.mBackupFlags);
mAccount.mBackupFlags = Account.BACKUP_FLAGS_IS_DEFAULT;
mAccount.save(mPreferences);
mAccount.mBackupFlags = -1;
mAccount.refresh(mPreferences);
assertEquals(Account.BACKUP_FLAGS_IS_DEFAULT, mAccount.mBackupFlags);
}
/**
* Create a dummy account with minimal fields
*/

View File

@ -16,6 +16,7 @@
package com.android.email;
import com.android.email.Account;
import com.android.email.mail.Address;
import com.android.email.mail.BodyPart;
import com.android.email.mail.Flag;
@ -65,13 +66,14 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
private static final String RECIPIENT_BCC = "recipient-bcc@android.com";
private static final String REPLY_TO = "reply-to@android.com";
private static final String SUBJECT = "This is the subject";
private static final String BODY = "This is the body. This is also the body.";
private static final String MESSAGE_ID = "Test-Message-ID";
private static final String MESSAGE_ID_2 = "Test-Message-ID-Second";
EmailProvider mProvider;
Context mProviderContext;
Context mContext;
Account mLegacyAccount = null;
Preferences mPreferences = null;
public LegacyConversionsTests() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
@ -87,6 +89,9 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
@Override
public void tearDown() throws Exception {
super.tearDown();
if (mLegacyAccount != null) {
mLegacyAccount.delete(mPreferences);
}
}
/**
@ -155,7 +160,7 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
}
if (sender != null) {
Address[] addresses = Address.parse(sender);
message.setFrom(Address.parse(sender)[0]);
message.setFrom(addresses[0]);
}
if (subject != null) {
message.setSubject(subject);
@ -480,4 +485,130 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
// cv.put("attachment_count", attachments.size());
}
/**
* Test conversion of a legacy account to a provider account
*/
public void testMakeProviderAccount() throws MessagingException {
setupLegacyAccount("testMakeProviderAccount", true);
EmailContent.Account toAccount =
LegacyConversions.makeAccount(mProviderContext, mLegacyAccount);
checkProviderAccount("testMakeProviderAccount", mLegacyAccount, toAccount);
}
/**
* Test conversion of a provider account to a legacy account
*/
public void testMakeLegacyAccount() throws MessagingException {
EmailContent.Account fromAccount = ProviderTestUtils.setupAccount("convert-to-legacy",
false, mProviderContext);
fromAccount.mHostAuthRecv =
ProviderTestUtils.setupHostAuth("legacy-recv", 0, false, mProviderContext);
fromAccount.mHostAuthSend =
ProviderTestUtils.setupHostAuth("legacy-send", 0, false, mProviderContext);
fromAccount.save(mProviderContext);
Account toAccount = LegacyConversions.makeLegacyAccount(mProviderContext, fromAccount);
checkLegacyAccount("testMakeLegacyAccount", fromAccount, toAccount);
}
/**
* Setup a legacy account in mLegacyAccount with many fields prefilled.
*/
private void setupLegacyAccount(String name, boolean saveIt) {
// prefs & legacy account are saved for cleanup (it's stored in the real prefs file)
mPreferences = Preferences.getPreferences(mProviderContext);
mLegacyAccount = new Account(mProviderContext);
// fill in useful fields
mLegacyAccount.mUuid = "test-uid-" + name;
mLegacyAccount.mStoreUri = "store://test/" + name;
mLegacyAccount.mLocalStoreUri = "local://localhost/" + name;
mLegacyAccount.mSenderUri = "sender://test/" + name;
mLegacyAccount.mDescription = "description " + name;
mLegacyAccount.mName = "name " + name;
mLegacyAccount.mEmail = "email " + name;
mLegacyAccount.mAutomaticCheckIntervalMinutes = 100;
mLegacyAccount.mLastAutomaticCheckTime = 200;
mLegacyAccount.mNotifyNewMail = true;
mLegacyAccount.mDraftsFolderName = "drafts " + name;
mLegacyAccount.mSentFolderName = "sent " + name;
mLegacyAccount.mTrashFolderName = "trash " + name;
mLegacyAccount.mOutboxFolderName = "outbox " + name;
mLegacyAccount.mAccountNumber = 300;
mLegacyAccount.mVibrate = true;
mLegacyAccount.mRingtoneUri = "ringtone://test/" + name;
mLegacyAccount.mSyncWindow = 400;
mLegacyAccount.mBackupFlags = 0;
mLegacyAccount.mDeletePolicy = Account.DELETE_POLICY_NEVER;
if (saveIt) {
mLegacyAccount.save(mPreferences);
}
}
/**
* Compare a provider account to the legacy account it was created from
*/
private void checkProviderAccount(String tag, Account expect, EmailContent.Account actual)
throws MessagingException {
assertEquals(tag + " description", expect.getDescription(), actual.mDisplayName);
assertEquals(tag + " email", expect.getEmail(), actual.mEmailAddress);
assertEquals(tag + " sync key", "", actual.mSyncKey);
assertEquals(tag + " lookback", expect.getSyncWindow(), actual.mSyncLookback);
assertEquals(tag + " sync intvl", expect.getAutomaticCheckIntervalMinutes(),
actual.mSyncInterval);
// These asserts are checking mHostAuthKeyRecv & mHostAuthKeySend
assertEquals(tag + " store", expect.getStoreUri(), actual.getStoreUri(mProviderContext));
assertEquals(tag + " sender", expect.getSenderUri(), actual.getSenderUri(mProviderContext));
// Synthesize & check flags
int expectFlags = 0;
if (expect.mNotifyNewMail) expectFlags |= EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL;
if (expect.mVibrate) expectFlags |= EmailContent.Account.FLAGS_VIBRATE;
expectFlags |=
(expect.mDeletePolicy << EmailContent.Account.FLAGS_DELETE_POLICY_SHIFT)
& EmailContent.Account.FLAGS_DELETE_POLICY_MASK;
assertEquals(tag + " flags", expectFlags, actual.mFlags);
assertEquals(tag + " default", false, actual.mIsDefault);
assertEquals(tag + " uuid", expect.getUuid(), actual.mCompatibilityUuid);
assertEquals(tag + " name", expect.getName(), actual.mSenderName);
assertEquals(tag + " ringtone", expect.getRingtone(), actual.mRingtoneUri);
assertEquals(tag + " proto vers", expect.mProtocolVersion, actual.mProtocolVersion);
assertEquals(tag + " new count", 0, actual.mNewMessageCount);
}
/**
* Compare a legacy account to the provider account it was created from
*/
private void checkLegacyAccount(String tag, EmailContent.Account expect, Account actual)
throws MessagingException {
int expectFlags = expect.getFlags();
assertEquals(tag + " uuid", expect.mCompatibilityUuid, actual.mUuid);
assertEquals(tag + " store", expect.getStoreUri(mProviderContext), actual.mStoreUri);
assertTrue(actual.mLocalStoreUri.startsWith("local://localhost"));
assertEquals(tag + " sender", expect.getSenderUri(mProviderContext), actual.mSenderUri);
assertEquals(tag + " description", expect.getDisplayName(), actual.mDescription);
assertEquals(tag + " name", expect.getSenderName(), actual.mName);
assertEquals(tag + " email", expect.getEmailAddress(), actual.mEmail);
assertEquals(tag + " checkintvl", expect.getSyncInterval(),
actual.mAutomaticCheckIntervalMinutes);
assertEquals(tag + " checktime", 0, actual.mLastAutomaticCheckTime);
assertEquals(tag + " notify",
(expectFlags & EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL) != 0,
actual.mNotifyNewMail);
assertEquals(tag + " drafts", null, actual.mDraftsFolderName);
assertEquals(tag + " sent", null, actual.mSentFolderName);
assertEquals(tag + " trash", null, actual.mTrashFolderName);
assertEquals(tag + " outbox", null, actual.mOutboxFolderName);
assertEquals(tag + " acct #", -1, actual.mAccountNumber);
assertEquals(tag + " vibrate", (expectFlags & EmailContent.Account.FLAGS_VIBRATE) != 0,
actual.mVibrate);
assertEquals(tag + " ", expect.getRingtone(), actual.mRingtoneUri);
assertEquals(tag + " sync window", expect.getSyncLookback(), actual.mSyncWindow);
assertEquals(tag + " backup flags", 0, actual.mBackupFlags);
assertEquals(tag + " proto vers", expect.mProtocolVersion, actual.mProtocolVersion);
assertEquals(tag + " delete policy", expect.getDeletePolicy(), actual.getDeletePolicy());
}
}

View File

@ -16,7 +16,6 @@
package com.android.email;
import android.content.SharedPreferences;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
@ -34,7 +33,6 @@ public class PreferencesUnitTests extends AndroidTestCase {
private Preferences mPreferences;
private String mUuid;
private Account mAccount;
@Override
@ -94,8 +92,6 @@ public class PreferencesUnitTests extends AndroidTestCase {
private void createTestAccount() {
mAccount = new Account(getContext());
mAccount.save(mPreferences);
mUuid = mAccount.getUuid();
}
}