am 151b9aa8: am 5d5d7854: Harden UpgradeAccounts against runtime errors (e.g. NPE)

Merge commit '151b9aa8dff6350c9e247579599bf70e1c55eea1' into kraken

* commit '151b9aa8dff6350c9e247579599bf70e1c55eea1':
  Harden UpgradeAccounts against runtime errors (e.g. NPE)
This commit is contained in:
Andrew Stadler 2010-04-21 18:19:27 -07:00 committed by Android Git Automerger
commit d729ff891a

View File

@ -38,7 +38,6 @@ import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.Mailbox;
import android.app.Activity;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
@ -65,6 +64,12 @@ import java.util.HashSet;
/**
* This activity will be used whenever we have a large/slow bulk upgrade operation.
*
* The general strategy is to iterate through the legacy accounts, convert them one-by-one, and
* then delete them. The loop is very conservative; If there is any problem, the bias will be
* to abandon the conversion and let the account be deleted. We never want to get stuck here, and
* we never want to run more than once (on a device being upgraded from 1.6). After this code
* runs, there should be zero legacy accounts.
*
* Note: It's preferable to check for "accounts needing upgrade" before launching this
* activity, so as to not waste time before every launch.
*
@ -171,17 +176,22 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Account account;
int maxProgress;
int progress;
String error;
String errorMessage; // set/read by handler - UI thread only
boolean isError; // set/read by worker thread
public AccountInfo(Account legacyAccount) {
account = legacyAccount;
maxProgress = 0;
progress = 0;
errorMessage = null;
isError = false;
}
}
private void loadAccountInfoArray(Account[] legacyAccounts) {
mLegacyAccounts = new AccountInfo[legacyAccounts.length];
for (int i = 0; i < legacyAccounts.length; i++) {
AccountInfo ai = new AccountInfo();
ai.account = legacyAccounts[i];
ai.maxProgress = 0;
ai.progress = 0;
ai.error = null;
AccountInfo ai = new AccountInfo(legacyAccounts[i]);
mLegacyAccounts[i] = ai;
}
}
@ -241,7 +251,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
ViewHolder vh = (ViewHolder) view.getTag();
AccountInfo ai = mLegacyAccounts[position];
vh.displayName.setText(ai.account.getDescription());
if (ai.error == null) {
if (ai.errorMessage == null) {
vh.errorReport.setVisibility(View.GONE);
vh.progress.setVisibility(View.VISIBLE);
vh.progress.setMax(ai.maxProgress);
@ -249,7 +259,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
} else {
vh.progress.setVisibility(View.GONE);
vh.errorReport.setVisibility(View.VISIBLE);
vh.errorReport.setText(ai.error);
vh.errorReport.setText(ai.errorMessage);
}
}
}
@ -282,7 +292,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
mListView.invalidateViews(); // find a less annoying way to do that
break;
case MSG_ERROR:
mLegacyAccounts[msg.arg1].error = (String) msg.obj;
mLegacyAccounts[msg.arg1].errorMessage = (String) msg.obj;
mListView.invalidateViews(); // find a less annoying way to do that
mProceedButton.setEnabled(true);
break;
@ -321,9 +331,10 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
}
// Note: also enables the "OK" button, so we pause when complete
public void error(String error) {
public void error(int accountNum, String error) {
android.os.Message msg = android.os.Message.obtain();
msg.what = MSG_ERROR;
msg.arg1 = accountNum;
msg.obj = error;
sendMessage(msg);
}
@ -338,7 +349,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
UpgradeAccounts.AccountInfo[] mAccountInfo;
final Context mContext;
final Preferences mPreferences;
public ConversionTask(UpgradeAccounts.AccountInfo[] accountInfo) {
// TODO: should I copy this?
mAccountInfo = accountInfo;
@ -362,17 +373,27 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
// Step 1: Analyze accounts and generate progress max values
for (int i = 0; i < mAccountInfo.length; i++) {
int estimate = UpgradeAccounts.estimateWork(mContext, mAccountInfo[i].account);
if (estimate == -1) {
mAccountInfo[i].isError = true;
mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
}
UpgradeAccounts.this.mHandler.setMaxProgress(i, estimate);
}
// Step 2: Scrub accounts, deleting anything we're not keeping to reclaim storage
for (int i = 0; i < mAccountInfo.length; i++) {
if (mAccountInfo[i].error == null) {
scrubAccount(mContext, mAccountInfo[i].account, i, handler);
if (!mAccountInfo[i].isError) {
boolean ok = scrubAccount(mContext, mAccountInfo[i].account, i, handler);
if (!ok) {
mAccountInfo[i].isError = true;
mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
}
}
}
// Step 3: Copy accounts (and delete old accounts). POP accounts first.
// Note: We don't check error condition here because we still want to
// delete the remaining parts of all accounts (even if in error condition).
for (int i = 0; i < mAccountInfo.length; i++) {
AccountInfo info = mAccountInfo[i];
copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_POP3);
@ -395,19 +416,31 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
* to force conversion of one or another type only.
*/
private void copyAndDeleteAccount(AccountInfo info, int i, UIHandler handler, String type) {
if (type != null) {
String storeUri = info.account.getStoreUri();
boolean isType = storeUri.startsWith(type);
if (!isType) {
return; // skip this account
try {
if (type != null) {
String storeUri = info.account.getStoreUri();
boolean isType = storeUri.startsWith(type);
if (!isType) {
return; // skip this account
}
}
// Don't try copying if this account is already in error state
if (!info.isError) {
copyAccount(mContext, info.account, i, handler);
}
} catch (RuntimeException e) {
Log.d(Email.LOG_TAG, "Exception while copying account " + e);
mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
info.isError = true;
}
if (info.error == null) {
copyAccount(mContext, info.account, i, handler);
// best effort to delete it (whether copied or not)
try {
deleteAccountStore(mContext, info.account, i, handler);
info.account.delete(mPreferences);
} catch (RuntimeException e) {
Log.d(Email.LOG_TAG, "Exception while deleting account " + e);
// No user notification is required here - we're done
}
deleteAccountStore(mContext, info.account, handler);
info.account.delete(mPreferences);
// jam the progress indicator to mark account "complete" (in case est was wrong)
handler.setProgress(i, Integer.MAX_VALUE);
}
@ -427,6 +460,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
/**
* Estimate the work required to convert an account.
* 1 (account) + # folders + # messages + # attachments
* @return conversion operations estimate, or -1 if there's any problem
*/
/* package */ static int estimateWork(Context context, Account account) {
int estimate = 1; // account
@ -444,6 +478,10 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
store.close();
} catch (MessagingException e) {
Log.d(Email.LOG_TAG, "Exception while estimating account size " + e);
return -1;
} catch (RuntimeException e) {
Log.d(Email.LOG_TAG, "Exception while estimating account size " + e);
return -1;
}
return estimate;
}
@ -454,13 +492,13 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
* For IMAP: Anything we can reload from server, we delete. This reduces the risk of running
* out of disk space by copying everything.
* For POP: Delete the trash folder (which we won't bring forward).
* @return true if successful, false if any kind of error
*/
/* package */ static void scrubAccount(Context context, Account account, int accountNum,
/* package */ static boolean scrubAccount(Context context, Account account, int accountNum,
UIHandler handler) {
String storeUri = account.getStoreUri();
boolean isImap = storeUri.startsWith(Store.STORE_SCHEME_IMAP);
try {
String storeUri = account.getStoreUri();
boolean isImap = storeUri.startsWith(Store.STORE_SCHEME_IMAP);
LocalStore store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
Folder[] folders = store.getPersonalNamespaces();
for (Folder folder : folders) {
@ -489,8 +527,13 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
}
store.close();
} catch (MessagingException e) {
Log.d(Email.LOG_TAG, "Exception while cleaning IMAP account " + e);
Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e);
return false;
} catch (RuntimeException e) {
Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e);
return false;
}
return true;
}
private static class FolderConversion {
@ -514,7 +557,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
if (existCount > 0) {
Log.d(Email.LOG_TAG, "No conversion, account exists: " + account.getDescription());
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
}
return;
}
@ -568,7 +611,8 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
// we'll try to keep going.
Log.d(Email.LOG_TAG, "Exception copying folder " + folderName + ": " + e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum,
context.getString(R.string.upgrade_accounts_error));
}
}
}
@ -597,7 +641,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Log.d(Email.LOG_TAG, "Exception while copying folders " + e);
// Couldn't copy folders at all
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
}
} finally {
if (store != null) {
@ -612,7 +656,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
* @param context a system context
* @param conversion a folder->mailbox conversion record
* @param localAttachments true if the attachments refer to local data (to be sent)
* @param newAccountId the id of the newly-created account
* @param newAccount the id of the newly-created account
* @param accountNum the UI list # of the account
* @param handler the handler for updating the UI
*/
@ -678,7 +722,8 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Log.d(Email.LOG_TAG, "Exception copying message " + oldMessage.getSubject()
+ ": "+ e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum,
context.getString(R.string.upgrade_accounts_error));
}
}
}
@ -687,7 +732,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Log.d(Email.LOG_TAG, "Exception while copying messages in " +
conversion.folder.toString() + ": " + e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
}
}
}
@ -695,7 +740,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
/**
* Delete an account
*/
/* package */ static void deleteAccountStore(Context context, Account account,
/* package */ static void deleteAccountStore(Context context, Account account, int accountNum,
UIHandler handler) {
try {
Store store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
@ -704,7 +749,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
} catch (MessagingException e) {
Log.d(Email.LOG_TAG, "Exception while deleting account " + e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
}
}
}
@ -795,8 +840,6 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
/**
* Test for bulk upgrades and return true if necessary
*
* TODO should be in an AsyncTask since it has DB ops
*
* @return true if upgrades required (old accounts exist). false otherwise.
*/
private static boolean bulkUpgradesRequired(Context context, Preferences preferences) {