Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.activity;
     18 
     19 import com.android.email.Account;
     20 import com.android.email.Email;
     21 import com.android.email.LegacyConversions;
     22 import com.android.email.Preferences;
     23 import com.android.email.R;
     24 import com.android.email.Utility;
     25 import com.android.email.activity.setup.AccountSettingsUtils;
     26 import com.android.email.activity.setup.AccountSettingsUtils.Provider;
     27 import com.android.email.mail.FetchProfile;
     28 import com.android.email.mail.Flag;
     29 import com.android.email.mail.Folder;
     30 import com.android.email.mail.Message;
     31 import com.android.email.mail.MessagingException;
     32 import com.android.email.mail.Part;
     33 import com.android.email.mail.Store;
     34 import com.android.email.mail.internet.MimeUtility;
     35 import com.android.email.mail.store.LocalStore;
     36 import com.android.email.provider.EmailContent;
     37 import com.android.email.provider.EmailContent.AccountColumns;
     38 import com.android.email.provider.EmailContent.HostAuth;
     39 import com.android.email.provider.EmailContent.Mailbox;
     40 
     41 import android.app.ListActivity;
     42 import android.content.Context;
     43 import android.content.Intent;
     44 import android.os.AsyncTask;
     45 import android.os.Bundle;
     46 import android.os.Handler;
     47 import android.util.Log;
     48 import android.view.LayoutInflater;
     49 import android.view.View;
     50 import android.view.ViewGroup;
     51 import android.view.View.OnClickListener;
     52 import android.widget.BaseAdapter;
     53 import android.widget.Button;
     54 import android.widget.ListView;
     55 import android.widget.ProgressBar;
     56 import android.widget.TextView;
     57 
     58 import java.io.IOException;
     59 import java.net.URI;
     60 import java.net.URISyntaxException;
     61 import java.util.ArrayList;
     62 import java.util.HashSet;
     63 
     64 /**
     65  * This activity will be used whenever we have a large/slow bulk upgrade operation.
     66  *
     67  * The general strategy is to iterate through the legacy accounts, convert them one-by-one, and
     68  * then delete them.  The loop is very conservative;  If there is any problem, the bias will be
     69  * to abandon the conversion and let the account be deleted.  We never want to get stuck here, and
     70  * we never want to run more than once (on a device being upgraded from 1.6).  After this code
     71  * runs, there should be zero legacy accounts.
     72  *
     73  * Note: It's preferable to check for "accounts needing upgrade" before launching this
     74  * activity, so as to not waste time before every launch.
     75  *
     76  * Note:  This activity is set (in the manifest) to disregard configuration changes (e.g. rotation).
     77  * This allows it to continue through without restarting.
     78  * Do not attempt to define orientation-specific resources, they won't be loaded.
     79  *
     80  * TODO: Read pending events and convert them to things like updates or deletes in the DB
     81  * TODO: Smarter cleanup of SSL/TLS situation, since certificates may be bad (see design spec)
     82  */
     83 public class UpgradeAccounts extends ListActivity implements OnClickListener {
     84 
     85     /** DO NOT CHECK IN AS 'TRUE' - DEVELOPMENT ONLY */
     86     private static final boolean DEBUG_FORCE_UPGRADES = false;
     87 
     88     private AccountInfo[] mLegacyAccounts;
     89     private UIHandler mHandler = new UIHandler();
     90     private AccountsAdapter mAdapter;
     91     private ListView mListView;
     92     private Button mProceedButton;
     93     private ConversionTask mConversionTask;
     94 
     95     // These are used to hold off restart of this activity while worker is still busy
     96     private static final Object sConversionInProgress = new Object();
     97     private static boolean sConversionHasRun = false;
     98 
     99     /** This projection is for looking up accounts by their legacy UUID */
    100     private static final String WHERE_ACCOUNT_UUID_IS = AccountColumns.COMPATIBILITY_UUID + "=?";
    101 
    102     public static void actionStart(Context context) {
    103         Intent i = new Intent(context, UpgradeAccounts.class);
    104         context.startActivity(i);
    105     }
    106 
    107     @Override
    108     public void onCreate(Bundle icicle) {
    109         super.onCreate(icicle);
    110 
    111         Preferences p = Preferences.getPreferences(this);
    112         Account[] legacyAccounts = p.getAccounts();
    113         if (legacyAccounts.length == 0) {
    114             finish();
    115             return;
    116         }
    117         loadAccountInfoArray(legacyAccounts);
    118 
    119         Log.d(Email.LOG_TAG, "*** Preparing to upgrade " +
    120                 Integer.toString(mLegacyAccounts.length) + " accounts");
    121 
    122         setContentView(R.layout.upgrade_accounts);
    123         mListView = getListView();
    124         mProceedButton = (Button) findViewById(R.id.action_button);
    125         mProceedButton.setEnabled(false);
    126         mProceedButton.setOnClickListener(this);
    127     }
    128 
    129     @Override
    130     protected void onResume() {
    131         super.onResume();
    132         updateList();
    133 
    134         // Start the big conversion engine
    135         mConversionTask = new ConversionTask(mLegacyAccounts);
    136         mConversionTask.execute();
    137     }
    138 
    139     @Override
    140     protected void onDestroy() {
    141         super.onDestroy();
    142 
    143         Utility.cancelTask(mConversionTask, false); // false = Don't interrupt running task
    144         mConversionTask = null;
    145     }
    146 
    147     /**
    148      * Stopgap measure to prevent monkey or zealous user from exiting while we're still at work.
    149      */
    150     @Override
    151     public void onBackPressed() {
    152         if (!mProceedButton.isEnabled()) {
    153             finish();
    154         }
    155     }
    156 
    157     public void onClick(View v) {
    158         switch (v.getId()) {
    159             case R.id.action_button:
    160                 onClickOk();
    161                 break;
    162         }
    163     }
    164 
    165     private void onClickOk() {
    166         Welcome.actionStart(UpgradeAccounts.this);
    167         finish();
    168     }
    169 
    170     private void updateList() {
    171         mAdapter = new AccountsAdapter();
    172         getListView().setAdapter(mAdapter);
    173     }
    174 
    175     private static class AccountInfo {
    176         Account account;
    177         int maxProgress;
    178         int progress;
    179         String errorMessage;    // set/read by handler - UI thread only
    180         boolean isError;        // set/read by worker thread
    181 
    182         public AccountInfo(Account legacyAccount) {
    183             account = legacyAccount;
    184             maxProgress = 0;
    185             progress = 0;
    186             errorMessage = null;
    187             isError = false;
    188         }
    189     }
    190 
    191     private void loadAccountInfoArray(Account[] legacyAccounts) {
    192         mLegacyAccounts = new AccountInfo[legacyAccounts.length];
    193         for (int i = 0; i < legacyAccounts.length; i++) {
    194             AccountInfo ai = new AccountInfo(legacyAccounts[i]);
    195             mLegacyAccounts[i] = ai;
    196         }
    197     }
    198 
    199     private static class ViewHolder {
    200         TextView displayName;
    201         ProgressBar progress;
    202         TextView errorReport;
    203     }
    204 
    205     class AccountsAdapter extends BaseAdapter {
    206         final LayoutInflater mInflater;
    207 
    208         AccountsAdapter() {
    209             mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    210         }
    211 
    212         @Override
    213         public boolean hasStableIds() {
    214             return true;
    215         }
    216 
    217         public int getCount() {
    218             return mLegacyAccounts.length;
    219         }
    220 
    221         public Object getItem(int position) {
    222             return mLegacyAccounts[position];
    223         }
    224 
    225         public long getItemId(int position) {
    226             return position;
    227         }
    228 
    229         public View getView(int position, View convertView, ViewGroup parent) {
    230             View v;
    231             if (convertView == null) {
    232                 v = newView(parent);
    233             } else {
    234                 v = convertView;
    235             }
    236             bindView(v, position);
    237             return v;
    238         }
    239 
    240         public View newView(ViewGroup parent) {
    241             View v = mInflater.inflate(R.layout.upgrade_accounts_item, parent, false);
    242             ViewHolder h = new ViewHolder();
    243             h.displayName = (TextView) v.findViewById(R.id.name);
    244             h.progress = (ProgressBar) v.findViewById(R.id.progress);
    245             h.errorReport = (TextView) v.findViewById(R.id.error);
    246             v.setTag(h);
    247             return v;
    248         }
    249 
    250         public void bindView(View view, int position) {
    251             ViewHolder vh = (ViewHolder) view.getTag();
    252             AccountInfo ai = mLegacyAccounts[position];
    253             vh.displayName.setText(ai.account.getDescription());
    254             if (ai.errorMessage == null) {
    255                 vh.errorReport.setVisibility(View.GONE);
    256                 vh.progress.setVisibility(View.VISIBLE);
    257                 vh.progress.setMax(ai.maxProgress);
    258                 vh.progress.setProgress(ai.progress);
    259             } else {
    260                 vh.progress.setVisibility(View.GONE);
    261                 vh.errorReport.setVisibility(View.VISIBLE);
    262                 vh.errorReport.setText(ai.errorMessage);
    263             }
    264         }
    265     }
    266 
    267     /**
    268      * Handler for updating UI from async workers
    269      *
    270      * TODO: I don't know the right paradigm for updating a progress bar in a ListView.  I'd
    271      * like to be able to say, "update it if it's visible, skip it if it's not visible."
    272      */
    273     class UIHandler extends Handler {
    274         private static final int MSG_SET_MAX = 1;
    275         private static final int MSG_SET_PROGRESS = 2;
    276         private static final int MSG_INC_PROGRESS = 3;
    277         private static final int MSG_ERROR = 4;
    278 
    279         @Override
    280         public void handleMessage(android.os.Message msg) {
    281             switch (msg.what) {
    282                 case MSG_SET_MAX:
    283                     mLegacyAccounts[msg.arg1].maxProgress = msg.arg2;
    284                     mListView.invalidateViews();        // find a less annoying way to do that
    285                     break;
    286                 case MSG_SET_PROGRESS:
    287                     mLegacyAccounts[msg.arg1].progress = msg.arg2;
    288                     mListView.invalidateViews();        // find a less annoying way to do that
    289                     break;
    290                 case MSG_INC_PROGRESS:
    291                     mLegacyAccounts[msg.arg1].progress += msg.arg2;
    292                     mListView.invalidateViews();        // find a less annoying way to do that
    293                     break;
    294                 case MSG_ERROR:
    295                     mLegacyAccounts[msg.arg1].errorMessage = (String) msg.obj;
    296                     mListView.invalidateViews();        // find a less annoying way to do that
    297                     mProceedButton.setEnabled(true);
    298                     break;
    299                 default:
    300                     super.handleMessage(msg);
    301             }
    302         }
    303 
    304         public void setMaxProgress(int accountNum, int max) {
    305             android.os.Message msg = android.os.Message.obtain();
    306             msg.what = MSG_SET_MAX;
    307             msg.arg1 = accountNum;
    308             msg.arg2 = max;
    309             sendMessage(msg);
    310         }
    311 
    312         public void setProgress(int accountNum, int progress) {
    313             android.os.Message msg = android.os.Message.obtain();
    314             msg.what = MSG_SET_PROGRESS;
    315             msg.arg1 = accountNum;
    316             msg.arg2 = progress;
    317             sendMessage(msg);
    318         }
    319 
    320         public void incProgress(int accountNum) {
    321             incProgress(accountNum, 1);
    322         }
    323 
    324         public void incProgress(int accountNum, int incrementBy) {
    325             if (incrementBy == 0) return;
    326             android.os.Message msg = android.os.Message.obtain();
    327             msg.what = MSG_INC_PROGRESS;
    328             msg.arg1 = accountNum;
    329             msg.arg2 = incrementBy;
    330             sendMessage(msg);
    331         }
    332 
    333         // Note: also enables the "OK" button, so we pause when complete
    334         public void error(int accountNum, String error) {
    335             android.os.Message msg = android.os.Message.obtain();
    336             msg.what = MSG_ERROR;
    337             msg.arg1 = accountNum;
    338             msg.obj = error;
    339             sendMessage(msg);
    340         }
    341     }
    342 
    343     /**
    344      * Everything above was UI plumbing.  This is the meat of this class - a conversion
    345      * engine to rebuild accounts from the "LocalStore" (pre Android 2.0) format to the
    346      * "Provider" (2.0 and beyond) format.
    347      */
    348     private class ConversionTask extends AsyncTask<Void, Void, Void> {
    349         UpgradeAccounts.AccountInfo[] mAccountInfo;
    350         final Context mContext;
    351         final Preferences mPreferences;
    352 
    353         public ConversionTask(UpgradeAccounts.AccountInfo[] accountInfo) {
    354             // TODO: should I copy this?
    355             mAccountInfo = accountInfo;
    356             mContext = UpgradeAccounts.this;
    357             mPreferences = Preferences.getPreferences(mContext);
    358         }
    359 
    360         @Override
    361         protected Void doInBackground(Void... params) {
    362             // Globally synchronize this entire block to prevent it from running in multiple
    363             // threads.  this is used in case we wind up relaunching during a conversion.
    364             // If this is anything but the first thread, sConversionHasRun will be set and we'll
    365             // exit immediately when it's all over.
    366             synchronized (sConversionInProgress) {
    367                 if (sConversionHasRun) {
    368                     return null;
    369                 }
    370                 sConversionHasRun = true;
    371 
    372                 UIHandler handler = UpgradeAccounts.this.mHandler;
    373                 // Step 1:  Analyze accounts and generate progress max values
    374                 for (int i = 0; i < mAccountInfo.length; i++) {
    375                     int estimate = UpgradeAccounts.estimateWork(mContext, mAccountInfo[i].account);
    376                     if (estimate == -1) {
    377                         mAccountInfo[i].isError = true;
    378                         mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
    379                     }
    380                     UpgradeAccounts.this.mHandler.setMaxProgress(i, estimate);
    381                 }
    382 
    383                 // Step 2:  Scrub accounts, deleting anything we're not keeping to reclaim storage
    384                 for (int i = 0; i < mAccountInfo.length; i++) {
    385                     if (!mAccountInfo[i].isError) {
    386                         boolean ok = scrubAccount(mContext, mAccountInfo[i].account, i, handler);
    387                         if (!ok) {
    388                             mAccountInfo[i].isError = true;
    389                             mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
    390                         }
    391                     }
    392                 }
    393 
    394                 // Step 3:  Copy accounts (and delete old accounts).  POP accounts first.
    395                 // Note:  We don't check error condition here because we still want to
    396                 // delete the remaining parts of all accounts (even if in error condition).
    397                 for (int i = 0; i < mAccountInfo.length; i++) {
    398                     AccountInfo info = mAccountInfo[i];
    399                     copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_POP3);
    400                 }
    401                 // IMAP accounts next.
    402                 for (int i = 0; i < mAccountInfo.length; i++) {
    403                     AccountInfo info = mAccountInfo[i];
    404                     copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_IMAP);
    405                 }
    406 
    407                 // Step 4:  Enable app-wide features such as composer, and start mail service(s)
    408                 Email.setServicesEnabled(mContext);
    409             }
    410 
    411             return null;
    412         }
    413 
    414         /**
    415          * Copy and delete one account (helper for doInBackground).  Can select accounts by type
    416          * to force conversion of one or another type only.
    417          */
    418         private void copyAndDeleteAccount(AccountInfo info, int i, UIHandler handler, String type) {
    419             try {
    420                 if (type != null) {
    421                     String storeUri = info.account.getStoreUri();
    422                     boolean isType = storeUri.startsWith(type);
    423                     if (!isType) {
    424                         return;         // skip this account
    425                     }
    426                 }
    427                 // Don't try copying if this account is already in error state
    428                 if (!info.isError) {
    429                     copyAccount(mContext, info.account, i, handler);
    430                 }
    431             } catch (RuntimeException e) {
    432                 Log.d(Email.LOG_TAG, "Exception while copying account " + e);
    433                 mHandler.error(i, mContext.getString(R.string.upgrade_accounts_error));
    434                 info.isError = true;
    435             }
    436             // best effort to delete it (whether copied or not)
    437             try {
    438                 deleteAccountStore(mContext, info.account, i, handler);
    439                 info.account.delete(mPreferences);
    440             } catch (RuntimeException e) {
    441                 Log.d(Email.LOG_TAG, "Exception while deleting account " + e);
    442                 // No user notification is required here - we're done
    443             }
    444             // jam the progress indicator to mark account "complete" (in case est was wrong)
    445             handler.setProgress(i, Integer.MAX_VALUE);
    446         }
    447 
    448         @Override
    449         protected void onPostExecute(Void result) {
    450             if (!isCancelled()) {
    451                 // if there were no errors, we never enabled the OK button, but
    452                 // we'll just proceed through anyway and return to the Welcome activity
    453                 if (!mProceedButton.isEnabled()) {
    454                     onClickOk();
    455                 }
    456             }
    457         }
    458     }
    459 
    460     /**
    461      * Estimate the work required to convert an account.
    462      * 1 (account) + # folders + # messages + # attachments
    463      * @return conversion operations estimate, or -1 if there's any problem
    464      */
    465     /* package */ static int estimateWork(Context context, Account account) {
    466         int estimate = 1;         // account
    467         try {
    468             LocalStore store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
    469             Folder[] folders = store.getPersonalNamespaces();
    470             estimate += folders.length;
    471             for (int i = 0; i < folders.length; i++) {
    472                 Folder folder = folders[i];
    473                 folder.open(Folder.OpenMode.READ_ONLY, null);
    474                 estimate += folder.getMessageCount();
    475                 folder.close(false);
    476             }
    477             estimate += store.getStoredAttachmentCount();
    478             store.close();
    479         } catch (MessagingException e) {
    480             Log.d(Email.LOG_TAG, "Exception while estimating account size " + e);
    481             return -1;
    482         } catch (RuntimeException e) {
    483             Log.d(Email.LOG_TAG, "Exception while estimating account size " + e);
    484             return -1;
    485         }
    486         return estimate;
    487     }
    488 
    489     /**
    490      * Clean out an account.
    491      *
    492      * For IMAP:  Anything we can reload from server, we delete.  This reduces the risk of running
    493      * out of disk space by copying everything.
    494      * For POP: Delete the trash folder (which we won't bring forward).
    495      * @return true if successful, false if any kind of error
    496      */
    497     /* package */ static boolean scrubAccount(Context context, Account account, int accountNum,
    498             UIHandler handler) {
    499         try {
    500             String storeUri = account.getStoreUri();
    501             boolean isImap = storeUri.startsWith(Store.STORE_SCHEME_IMAP);
    502             LocalStore store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
    503             Folder[] folders = store.getPersonalNamespaces();
    504             for (Folder folder : folders) {
    505                 folder.open(Folder.OpenMode.READ_ONLY, null);
    506                 String folderName = folder.getName();
    507                 if ("drafts".equalsIgnoreCase(folderName)) {
    508                     // do not delete drafts
    509                 } else if ("outbox".equalsIgnoreCase(folderName)) {
    510                     // do not delete outbox
    511                 } else if ("sent".equalsIgnoreCase(folderName)) {
    512                     // do not delete sent
    513                 } else if (isImap || "trash".equalsIgnoreCase(folderName)) {
    514                     Log.d(Email.LOG_TAG, "Scrub " + account.getDescription() + "." + folderName);
    515                     // for all other folders, delete the folder (and its messages & attachments)
    516                     int messageCount = folder.getMessageCount();
    517                     folder.delete(true);
    518                     if (handler != null) {
    519                         handler.incProgress(accountNum, 1 + messageCount);
    520                     }
    521                 }
    522                 folder.close(false);
    523             }
    524             int pruned = store.pruneCachedAttachments();
    525             if (handler != null) {
    526                 handler.incProgress(accountNum, pruned);
    527             }
    528             store.close();
    529         } catch (MessagingException e) {
    530             Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e);
    531             return false;
    532         } catch (RuntimeException e) {
    533             Log.d(Email.LOG_TAG, "Exception while scrubbing account " + e);
    534             return false;
    535         }
    536         return true;
    537     }
    538 
    539     private static class FolderConversion {
    540         final Folder folder;
    541         final EmailContent.Mailbox mailbox;
    542 
    543         public FolderConversion(Folder _folder, EmailContent.Mailbox _mailbox) {
    544             folder = _folder;
    545             mailbox = _mailbox;
    546         }
    547     }
    548 
    549     /**
    550      * Copy an account.
    551      */
    552     /* package */ static void copyAccount(Context context, Account account, int accountNum,
    553             UIHandler handler) {
    554         // If already exists- just skip it
    555         int existCount = EmailContent.count(context, EmailContent.Account.CONTENT_URI,
    556                 WHERE_ACCOUNT_UUID_IS, new String[] { account.getUuid() });
    557         if (existCount > 0) {
    558             Log.d(Email.LOG_TAG, "No conversion, account exists: " + account.getDescription());
    559             if (handler != null) {
    560                 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
    561             }
    562             return;
    563         }
    564         // Create the new account and write it
    565         EmailContent.Account newAccount = LegacyConversions.makeAccount(context, account);
    566         cleanupConnections(context, newAccount, account);
    567         newAccount.save(context);
    568         if (handler != null) {
    569             handler.incProgress(accountNum);
    570         }
    571 
    572         // copy the folders, making a set of them as we go, and recording a few that we
    573         // need to process first (highest priority for saving the messages)
    574         HashSet<FolderConversion> conversions = new HashSet<FolderConversion>();
    575         FolderConversion drafts = null;
    576         FolderConversion outbox = null;
    577         FolderConversion sent = null;
    578         LocalStore store = null;
    579         try {
    580             store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
    581             Folder[] folders = store.getPersonalNamespaces();
    582             for (Folder folder : folders) {
    583                 String folderName = null;
    584                 try {
    585                     folder.open(Folder.OpenMode.READ_ONLY, null);
    586                     folderName = folder.getName();
    587                     Log.d(Email.LOG_TAG, "Copy " + account.getDescription() + "." + folderName);
    588                     EmailContent.Mailbox mailbox =
    589                         LegacyConversions.makeMailbox(context, newAccount, folder);
    590                     mailbox.save(context);
    591                     if (handler != null) {
    592                         handler.incProgress(accountNum);
    593                     }
    594                     folder.close(false);
    595                     // Now record the conversion, to come back and do the messages
    596                     FolderConversion conversion = new FolderConversion(folder, mailbox);
    597                     conversions.add(conversion);
    598                     switch (mailbox.mType) {
    599                         case Mailbox.TYPE_DRAFTS:
    600                             drafts = conversion;
    601                             break;
    602                         case Mailbox.TYPE_OUTBOX:
    603                             outbox = conversion;
    604                             break;
    605                         case Mailbox.TYPE_SENT:
    606                             sent = conversion;
    607                             break;
    608                     }
    609                 } catch (MessagingException e) {
    610                     // We make a best-effort attempt at each folder, so even if this one fails,
    611                     // we'll try to keep going.
    612                     Log.d(Email.LOG_TAG, "Exception copying folder " + folderName + ": " + e);
    613                     if (handler != null) {
    614                         handler.error(accountNum,
    615                                 context.getString(R.string.upgrade_accounts_error));
    616                     }
    617                 }
    618             }
    619 
    620             // copy the messages, starting with the most critical folders, and then doing the rest
    621             // outbox & drafts are the most important, as they don't exist anywhere else.  we also
    622             // process local (outgoing) attachments here
    623             if (outbox != null) {
    624                 copyMessages(context, outbox, true, newAccount, accountNum, handler);
    625                 conversions.remove(outbox);
    626             }
    627             if (drafts != null) {
    628                 copyMessages(context, drafts, true, newAccount, accountNum, handler);
    629                 conversions.remove(drafts);
    630             }
    631             if (sent != null) {
    632                 copyMessages(context, sent, true, newAccount, accountNum, handler);
    633                 conversions.remove(outbox);
    634             }
    635             // Now handle any remaining folders.  For incoming folders we skip attachments, as they
    636             // can be reloaded from the server.
    637             for (FolderConversion conversion : conversions) {
    638                 copyMessages(context, conversion, false, newAccount, accountNum, handler);
    639             }
    640         } catch (MessagingException e) {
    641             Log.d(Email.LOG_TAG, "Exception while copying folders " + e);
    642             // Couldn't copy folders at all
    643             if (handler != null) {
    644                 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
    645             }
    646         } finally {
    647             if (store != null) {
    648                 store.close();
    649             }
    650         }
    651     }
    652 
    653     /**
    654      * Copy all messages in a given folder
    655      *
    656      * @param context a system context
    657      * @param conversion a folder->mailbox conversion record
    658      * @param localAttachments true if the attachments refer to local data (to be sent)
    659      * @param newAccount the id of the newly-created account
    660      * @param accountNum the UI list # of the account
    661      * @param handler the handler for updating the UI
    662      */
    663     /* package */ static void copyMessages(Context context, FolderConversion conversion,
    664             boolean localAttachments, EmailContent.Account newAccount, int accountNum,
    665             UIHandler handler) {
    666         try {
    667             boolean makeDeleteSentinels = (conversion.mailbox.mType == Mailbox.TYPE_INBOX) &&
    668                     (newAccount.getDeletePolicy() == EmailContent.Account.DELETE_POLICY_NEVER);
    669             conversion.folder.open(Folder.OpenMode.READ_ONLY, null);
    670             Message[] oldMessages = conversion.folder.getMessages(null);
    671             for (Message oldMessage : oldMessages) {
    672                 Exception e = null;
    673                 try {
    674                     // load message data from legacy Store
    675                     FetchProfile fp = new FetchProfile();
    676                     fp.add(FetchProfile.Item.ENVELOPE);
    677                     fp.add(FetchProfile.Item.BODY);
    678                     conversion.folder.fetch(new Message[] { oldMessage }, fp, null);
    679                     EmailContent.Message newMessage = new EmailContent.Message();
    680                     if (makeDeleteSentinels && oldMessage.isSet(Flag.DELETED)) {
    681                         // Special case for POP3 locally-deleted messages.
    682                         // Creates a local "deleted message sentinel" which hides the message
    683                         // Echos provider code in MessagingController.processPendingMoveToTrash()
    684                         newMessage.mAccountKey = newAccount.mId;
    685                         newMessage.mMailboxKey = conversion.mailbox.mId;
    686                         newMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED;
    687                         newMessage.mFlagRead = true;
    688                         newMessage.mServerId = oldMessage.getUid();
    689                         newMessage.save(context);
    690                     } else {
    691                         // Main case for converting real messages with bodies & attachments
    692                         // convert message (headers)
    693                         LegacyConversions.updateMessageFields(newMessage, oldMessage,
    694                                 newAccount.mId, conversion.mailbox.mId);
    695                         // convert body (text)
    696                         EmailContent.Body newBody = new EmailContent.Body();
    697                         ArrayList<Part> viewables = new ArrayList<Part>();
    698                         ArrayList<Part> attachments = new ArrayList<Part>();
    699                         MimeUtility.collectParts(oldMessage, viewables, attachments);
    700                         LegacyConversions.updateBodyFields(newBody, newMessage, viewables);
    701                         // commit changes so far so we have real id's
    702                         newMessage.save(context);
    703                         newBody.save(context);
    704                         // convert attachments
    705                         if (localAttachments) {
    706                             // These are references to local data, and should create records only
    707                             // (e.g. the content URI).  No files should be created.
    708                             LegacyConversions.updateAttachments(context, newMessage, attachments,
    709                                     true);
    710                         }
    711                     }
    712                     // done
    713                     if (handler != null) {
    714                         handler.incProgress(accountNum);
    715                     }
    716                 } catch (MessagingException me) {
    717                     e = me;
    718                 } catch (IOException ioe) {
    719                     e = ioe;
    720                 }
    721                 if (e != null) {
    722                     Log.d(Email.LOG_TAG, "Exception copying message " + oldMessage.getSubject()
    723                             + ": "+ e);
    724                     if (handler != null) {
    725                         handler.error(accountNum,
    726                                 context.getString(R.string.upgrade_accounts_error));
    727                     }
    728                 }
    729             }
    730         } catch (MessagingException e) {
    731             // Couldn't copy folder at all
    732             Log.d(Email.LOG_TAG, "Exception while copying messages in " +
    733                     conversion.folder.toString() + ": " + e);
    734             if (handler != null) {
    735                 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
    736             }
    737         }
    738     }
    739 
    740     /**
    741      * Delete an account
    742      */
    743     /* package */ static void deleteAccountStore(Context context, Account account, int accountNum,
    744             UIHandler handler) {
    745         try {
    746             Store store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
    747             store.delete();
    748             // delete() closes the store
    749         } catch (MessagingException e) {
    750             Log.d(Email.LOG_TAG, "Exception while deleting account " + e);
    751             if (handler != null) {
    752                 handler.error(accountNum, context.getString(R.string.upgrade_accounts_error));
    753             }
    754         }
    755     }
    756 
    757     /**
    758      * Cleanup SSL, TLS, etc for each converted account.
    759      */
    760     /* package */ static void cleanupConnections(Context context, EmailContent.Account newAccount,
    761             Account account) {
    762         // 1. Look up provider for this email address
    763         String email = newAccount.mEmailAddress;
    764         int atSignPos = email.lastIndexOf('@');
    765         String domain = email.substring(atSignPos + 1);
    766         Provider p = AccountSettingsUtils.findProviderForDomain(context, domain);
    767 
    768         // 2. If provider found, just use its settings (overriding what user had)
    769         // This is drastic but most reliable.  Note:  This also benefits from newer provider
    770         // data that might be found in a vendor policy module.
    771         if (p != null) {
    772             // Incoming
    773             try {
    774                 URI incomingUriTemplate = p.incomingUriTemplate;
    775                 String incomingUsername = newAccount.mHostAuthRecv.mLogin;
    776                 String incomingPassword = newAccount.mHostAuthRecv.mPassword;
    777                 URI incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":"
    778                         + incomingPassword, incomingUriTemplate.getHost(),
    779                         incomingUriTemplate.getPort(), incomingUriTemplate.getPath(), null, null);
    780                 newAccount.mHostAuthRecv.setStoreUri(incomingUri.toString());
    781             } catch (URISyntaxException e) {
    782                 // Ignore - just use the data we copied across (for better or worse)
    783             }
    784             // Outgoing
    785             try {
    786                 URI outgoingUriTemplate = p.outgoingUriTemplate;
    787                 String outgoingUsername = newAccount.mHostAuthSend.mLogin;
    788                 String outgoingPassword = newAccount.mHostAuthSend.mPassword;
    789                 URI outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":"
    790                         + outgoingPassword, outgoingUriTemplate.getHost(),
    791                         outgoingUriTemplate.getPort(), outgoingUriTemplate.getPath(), null, null);
    792                 newAccount.mHostAuthSend.setStoreUri(outgoingUri.toString());
    793             } catch (URISyntaxException e) {
    794                 // Ignore - just use the data we copied across (for better or worse)
    795             }
    796             Log.d(Email.LOG_TAG, "Rewriting connection details for " + account.getDescription());
    797             return;
    798         }
    799 
    800         // 3. Otherwise, use simple heuristics to adjust connection and attempt to keep it
    801         //    reliable.  NOTE:  These are the "legacy" ssl/tls encodings, not the ones in
    802         //    the current provider.
    803         newAccount.mHostAuthRecv.mFlags |= HostAuth.FLAG_TRUST_ALL_CERTIFICATES;
    804         String receiveUri = account.getStoreUri();
    805         if (receiveUri.contains("+ssl+")) {
    806             // non-optional SSL - keep as is, with trust all
    807         } else if (receiveUri.contains("+ssl")) {
    808             // optional SSL - TBD
    809         } else if (receiveUri.contains("+tls+")) {
    810             // non-optional TLS - keep as is, with trust all
    811         } else if (receiveUri.contains("+tls")) {
    812             // optional TLS - TBD
    813         }
    814         newAccount.mHostAuthSend.mFlags |= HostAuth.FLAG_TRUST_ALL_CERTIFICATES;
    815         String sendUri = account.getSenderUri();
    816         if (sendUri.contains("+ssl+")) {
    817             // non-optional SSL - keep as is, with trust all
    818         } else if (sendUri.contains("+ssl")) {
    819             // optional SSL - TBD
    820         } else if (sendUri.contains("+tls+")) {
    821             // non-optional TLS - keep as is, with trust all
    822         } else if (sendUri.contains("+tls")) {
    823             // optional TLS - TBD
    824         }
    825     }
    826 
    827     /**
    828      * Bulk upgrade old accounts if exist.
    829      *
    830      * @return true if bulk upgrade has started.  false otherwise.
    831      */
    832     /* package */ static boolean doBulkUpgradeIfNecessary(Context context) {
    833         if (bulkUpgradesRequired(context, Preferences.getPreferences(context))) {
    834             UpgradeAccounts.actionStart(context);
    835             return true;
    836         }
    837         return false;
    838     }
    839 
    840     /**
    841      * Test for bulk upgrades and return true if necessary
    842      *
    843      * @return true if upgrades required (old accounts exist).  false otherwise.
    844      */
    845     private static boolean bulkUpgradesRequired(Context context, Preferences preferences) {
    846         if (DEBUG_FORCE_UPGRADES) {
    847             // build at least one fake account
    848             Account fake = new Account(context);
    849             fake.setDescription("Fake Account");
    850             fake.setEmail("user (at) gmail.com");
    851             fake.setName("First Last");
    852             fake.setSenderUri("smtp://user:password (at) smtp.gmail.com");
    853             fake.setStoreUri("imap://user:password (at) imap.gmail.com");
    854             fake.save(preferences);
    855             return true;
    856         }
    857 
    858         // 1. Get list of legacy accounts and look for any non-backup entries
    859         Account[] legacyAccounts = preferences.getAccounts();
    860         if (legacyAccounts.length == 0) {
    861             return false;
    862         }
    863 
    864         // 2. Look at the first legacy account and decide what to do
    865         // We only need to look at the first:  If it's not a backup account, then it's a true
    866         // legacy account, and there are one or more accounts needing upgrade.  If it is a backup
    867         // account, then we know for sure that there are no legacy accounts (backup deletes all
    868         // old accounts, and indicates that "modern" code has already run on this device.)
    869         if (0 != (legacyAccounts[0].getBackupFlags() & Account.BACKUP_FLAGS_IS_BACKUP)) {
    870             return false;
    871         } else {
    872             return true;
    873         }
    874     }
    875 }
    876