Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2009 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.provider;
     18 
     19 import android.appwidget.AppWidgetManager;
     20 import android.content.ComponentCallbacks;
     21 import android.content.ComponentName;
     22 import android.content.ContentProvider;
     23 import android.content.ContentProviderOperation;
     24 import android.content.ContentProviderResult;
     25 import android.content.ContentResolver;
     26 import android.content.ContentUris;
     27 import android.content.ContentValues;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.OperationApplicationException;
     31 import android.content.PeriodicSync;
     32 import android.content.UriMatcher;
     33 import android.content.pm.ActivityInfo;
     34 import android.content.res.Configuration;
     35 import android.content.res.Resources;
     36 import android.database.ContentObserver;
     37 import android.database.Cursor;
     38 import android.database.CursorWrapper;
     39 import android.database.DatabaseUtils;
     40 import android.database.MatrixCursor;
     41 import android.database.MergeCursor;
     42 import android.database.sqlite.SQLiteDatabase;
     43 import android.database.sqlite.SQLiteException;
     44 import android.net.Uri;
     45 import android.os.AsyncTask;
     46 import android.os.Binder;
     47 import android.os.Build;
     48 import android.os.Bundle;
     49 import android.os.Handler;
     50 import android.os.Handler.Callback;
     51 import android.os.Looper;
     52 import android.os.Parcel;
     53 import android.os.ParcelFileDescriptor;
     54 import android.os.RemoteException;
     55 import android.provider.BaseColumns;
     56 import android.text.TextUtils;
     57 import android.text.format.DateUtils;
     58 import android.text.util.Rfc822Token;
     59 import android.text.util.Rfc822Tokenizer;
     60 import android.util.Base64;
     61 import android.util.Log;
     62 import android.util.SparseArray;
     63 
     64 import com.android.common.content.ProjectionMap;
     65 import com.android.email.Preferences;
     66 import com.android.email.R;
     67 import com.android.email.SecurityPolicy;
     68 import com.android.email.service.AttachmentDownloadService;
     69 import com.android.email.service.EmailServiceUtils;
     70 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
     71 import com.android.email2.ui.MailActivityEmail;
     72 import com.android.emailcommon.Logging;
     73 import com.android.emailcommon.mail.Address;
     74 import com.android.emailcommon.provider.Account;
     75 import com.android.emailcommon.provider.EmailContent;
     76 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     77 import com.android.emailcommon.provider.EmailContent.Attachment;
     78 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     79 import com.android.emailcommon.provider.EmailContent.Body;
     80 import com.android.emailcommon.provider.EmailContent.BodyColumns;
     81 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
     82 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     83 import com.android.emailcommon.provider.EmailContent.Message;
     84 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     85 import com.android.emailcommon.provider.EmailContent.PolicyColumns;
     86 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     87 import com.android.emailcommon.provider.HostAuth;
     88 import com.android.emailcommon.provider.Mailbox;
     89 import com.android.emailcommon.provider.MailboxUtilities;
     90 import com.android.emailcommon.provider.MessageChangeLogTable;
     91 import com.android.emailcommon.provider.MessageMove;
     92 import com.android.emailcommon.provider.MessageStateChange;
     93 import com.android.emailcommon.provider.Policy;
     94 import com.android.emailcommon.provider.QuickResponse;
     95 import com.android.emailcommon.service.EmailServiceProxy;
     96 import com.android.emailcommon.service.EmailServiceStatus;
     97 import com.android.emailcommon.service.IEmailService;
     98 import com.android.emailcommon.service.SearchParams;
     99 import com.android.emailcommon.utility.AttachmentUtilities;
    100 import com.android.emailcommon.utility.Utility;
    101 import com.android.ex.photo.provider.PhotoContract;
    102 import com.android.mail.preferences.MailPrefs;
    103 import com.android.mail.providers.ConversationInfo;
    104 import com.android.mail.providers.Folder;
    105 import com.android.mail.providers.FolderList;
    106 import com.android.mail.providers.MessageInfo;
    107 import com.android.mail.providers.UIProvider;
    108 import com.android.mail.providers.UIProvider.AccountCapabilities;
    109 import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
    110 import com.android.mail.providers.UIProvider.ConversationColumns;
    111 import com.android.mail.providers.UIProvider.ConversationPriority;
    112 import com.android.mail.providers.UIProvider.ConversationSendingState;
    113 import com.android.mail.providers.UIProvider.DraftType;
    114 import com.android.mail.utils.AttachmentUtils;
    115 import com.android.mail.utils.LogTag;
    116 import com.android.mail.utils.LogUtils;
    117 import com.android.mail.utils.MatrixCursorWithCachedColumns;
    118 import com.android.mail.utils.MatrixCursorWithExtra;
    119 import com.android.mail.utils.MimeType;
    120 import com.android.mail.utils.Utils;
    121 import com.android.mail.widget.BaseWidgetProvider;
    122 import com.google.common.collect.ImmutableMap;
    123 import com.google.common.collect.ImmutableSet;
    124 import com.google.common.collect.Lists;
    125 
    126 import java.io.File;
    127 import java.io.FileDescriptor;
    128 import java.io.FileNotFoundException;
    129 import java.io.PrintWriter;
    130 import java.util.ArrayList;
    131 import java.util.Arrays;
    132 import java.util.HashSet;
    133 import java.util.List;
    134 import java.util.Locale;
    135 import java.util.Map;
    136 import java.util.Set;
    137 import java.util.regex.Pattern;
    138 
    139 /**
    140  * @author mblank
    141  *
    142  */
    143 public class EmailProvider extends ContentProvider {
    144 
    145     private static final String TAG = LogTag.getLogTag();
    146 
    147     // Time to delay upsync requests.
    148     public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
    149 
    150     public static String EMAIL_APP_MIME_TYPE;
    151 
    152     // exposed for testing
    153     public static final String DATABASE_NAME = "EmailProvider.db";
    154     public static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
    155 
    156     private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db";
    157 
    158     /**
    159      * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this
    160      * {@link android.content.Intent} and update accordingly. However, this can be very broad and
    161      * is NOT the preferred way of getting notification.
    162      */
    163     private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED =
    164         "com.android.email.MESSAGE_LIST_DATASET_CHANGED";
    165 
    166     private static final String EMAIL_MESSAGE_MIME_TYPE =
    167         "vnd.android.cursor.item/email-message";
    168     private static final String EMAIL_ATTACHMENT_MIME_TYPE =
    169         "vnd.android.cursor.item/email-attachment";
    170 
    171     /** Appended to the notification URI for delete operations */
    172     private static final String NOTIFICATION_OP_DELETE = "delete";
    173     /** Appended to the notification URI for insert operations */
    174     private static final String NOTIFICATION_OP_INSERT = "insert";
    175     /** Appended to the notification URI for update operations */
    176     private static final String NOTIFICATION_OP_UPDATE = "update";
    177 
    178     /** The query string to trigger a folder refresh. */
    179     private static String QUERY_UIREFRESH = "uirefresh";
    180 
    181     // Definitions for our queries looking for orphaned messages
    182     private static final String[] ORPHANS_PROJECTION
    183         = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY};
    184     private static final int ORPHANS_ID = 0;
    185     private static final int ORPHANS_MAILBOX_KEY = 1;
    186 
    187     private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
    188 
    189     private static final int ACCOUNT_BASE = 0;
    190     private static final int ACCOUNT = ACCOUNT_BASE;
    191     private static final int ACCOUNT_ID = ACCOUNT_BASE + 1;
    192     private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 2;
    193     private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 3;
    194     private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 4;
    195     private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 5;
    196     private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 6;
    197 
    198     private static final int MAILBOX_BASE = 0x1000;
    199     private static final int MAILBOX = MAILBOX_BASE;
    200     private static final int MAILBOX_ID = MAILBOX_BASE + 1;
    201     private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2;
    202     private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3;
    203     private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4;
    204 
    205     private static final int MESSAGE_BASE = 0x2000;
    206     private static final int MESSAGE = MESSAGE_BASE;
    207     private static final int MESSAGE_ID = MESSAGE_BASE + 1;
    208     private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
    209     private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3;
    210     private static final int MESSAGE_MOVE = MESSAGE_BASE + 4;
    211     private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5;
    212 
    213     private static final int ATTACHMENT_BASE = 0x3000;
    214     private static final int ATTACHMENT = ATTACHMENT_BASE;
    215     private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1;
    216     private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2;
    217     private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3;
    218 
    219     private static final int HOSTAUTH_BASE = 0x4000;
    220     private static final int HOSTAUTH = HOSTAUTH_BASE;
    221     private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
    222 
    223     private static final int UPDATED_MESSAGE_BASE = 0x5000;
    224     private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
    225     private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
    226 
    227     private static final int DELETED_MESSAGE_BASE = 0x6000;
    228     private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
    229     private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
    230 
    231     private static final int POLICY_BASE = 0x7000;
    232     private static final int POLICY = POLICY_BASE;
    233     private static final int POLICY_ID = POLICY_BASE + 1;
    234 
    235     private static final int QUICK_RESPONSE_BASE = 0x8000;
    236     private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE;
    237     private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1;
    238     private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2;
    239 
    240     private static final int UI_BASE = 0x9000;
    241     private static final int UI_FOLDERS = UI_BASE;
    242     private static final int UI_SUBFOLDERS = UI_BASE + 1;
    243     private static final int UI_MESSAGES = UI_BASE + 2;
    244     private static final int UI_MESSAGE = UI_BASE + 3;
    245     private static final int UI_UNDO = UI_BASE + 4;
    246     private static final int UI_FOLDER_REFRESH = UI_BASE + 5;
    247     private static final int UI_FOLDER = UI_BASE + 6;
    248     private static final int UI_ACCOUNT = UI_BASE + 7;
    249     private static final int UI_ACCTS = UI_BASE + 8;
    250     private static final int UI_ATTACHMENTS = UI_BASE + 9;
    251     private static final int UI_ATTACHMENT = UI_BASE + 10;
    252     private static final int UI_SEARCH = UI_BASE + 11;
    253     private static final int UI_ACCOUNT_DATA = UI_BASE + 12;
    254     private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 13;
    255     private static final int UI_CONVERSATION = UI_BASE + 14;
    256     private static final int UI_RECENT_FOLDERS = UI_BASE + 15;
    257     private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 16;
    258     private static final int UI_FULL_FOLDERS = UI_BASE + 17;
    259     private static final int UI_ALL_FOLDERS = UI_BASE + 18;
    260 
    261     private static final int BODY_BASE = 0xA000;
    262     private static final int BODY = BODY_BASE;
    263     private static final int BODY_ID = BODY_BASE + 1;
    264 
    265     private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
    266 
    267     private static final SparseArray<String> TABLE_NAMES;
    268     static {
    269         SparseArray<String> array = new SparseArray<String>(11);
    270         array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME);
    271         array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME);
    272         array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME);
    273         array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME);
    274         array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME);
    275         array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME);
    276         array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME);
    277         array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME);
    278         array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME);
    279         array.put(UI_BASE >> BASE_SHIFT, null);
    280         array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME);
    281         TABLE_NAMES = array;
    282     }
    283 
    284     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    285 
    286     /**
    287      * Functions which manipulate the database connection or files synchronize on this.
    288      * It's static because there can be multiple provider objects.
    289      * TODO: Do we actually need to synchronize across all DB access, not just connection creation?
    290      */
    291     private static final Object sDatabaseLock = new Object();
    292 
    293     /**
    294      * Let's only generate these SQL strings once, as they are used frequently
    295      * Note that this isn't relevant for table creation strings, since they are used only once
    296      */
    297     private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
    298         Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    299         EmailContent.RECORD_ID + '=';
    300 
    301     private static final String UPDATED_MESSAGE_DELETE = "delete from " +
    302         Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '=';
    303 
    304     private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
    305         Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    306         EmailContent.RECORD_ID + '=';
    307 
    308     private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
    309         " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY +
    310         " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " +
    311         Message.TABLE_NAME + ')';
    312 
    313     private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
    314         " where " + BodyColumns.MESSAGE_KEY + '=';
    315 
    316     private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
    317     private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
    318 
    319     private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId";
    320 
    321     // For undo handling
    322     private int mLastSequence = -1;
    323     private final ArrayList<ContentProviderOperation> mLastSequenceOps =
    324             new ArrayList<ContentProviderOperation>();
    325 
    326     // Query parameter indicating the command came from UIProvider
    327     private static final String IS_UIPROVIDER = "is_uiprovider";
    328 
    329     private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status";
    330 
    331     /**
    332      * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
    333      * @param uri the Uri to match
    334      * @return the match value
    335      */
    336     private static int findMatch(Uri uri, String methodName) {
    337         int match = sURIMatcher.match(uri);
    338         if (match < 0) {
    339             throw new IllegalArgumentException("Unknown uri: " + uri);
    340         } else if (Logging.LOGD) {
    341             LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
    342         }
    343         return match;
    344     }
    345 
    346     // exposed for testing
    347     public static Uri INTEGRITY_CHECK_URI;
    348 
    349     public static Uri ACCOUNT_BACKUP_URI;
    350     private static Uri FOLDER_STATUS_URI;
    351 
    352     private SQLiteDatabase mDatabase;
    353     private SQLiteDatabase mBodyDatabase;
    354 
    355     private Handler mDelayedSyncHandler;
    356     private final Set<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>();
    357 
    358     public static Uri uiUri(String type, long id) {
    359         return Uri.parse(uiUriString(type, id));
    360     }
    361 
    362     /**
    363      * Creates a URI string from a database ID (guaranteed to be unique).
    364      * @param type of the resource: uifolder, message, etc.
    365      * @param id the id of the resource.
    366      * @return uri string
    367      */
    368     public static String uiUriString(String type, long id) {
    369         return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id));
    370     }
    371 
    372     /**
    373      * Orphan record deletion utility.  Generates a sqlite statement like:
    374      *  delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>)
    375      * Exposed for testing.
    376      * @param db the EmailProvider database
    377      * @param table the table whose orphans are to be removed
    378      * @param column the column deletion will be based on
    379      * @param foreignColumn the column in the foreign table whose absence will trigger the deletion
    380      * @param foreignTable the foreign table
    381      */
    382     public static void deleteUnlinked(SQLiteDatabase db, String table, String column,
    383             String foreignColumn, String foreignTable) {
    384         int count = db.delete(table, column + " not in (select " + foreignColumn + " from " +
    385                 foreignTable + ")", null);
    386         if (count > 0) {
    387             LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table);
    388         }
    389     }
    390 
    391 
    392     /**
    393      * Make sure that parentKeys match with parentServerId.
    394      * When we sync folders, we do two passes: First to create the mailbox rows, and second
    395      * to set the parentKeys. Two passes are needed because we won't know the parent's Id
    396      * until that row is inserted, and the order in which the rows are given is arbitrary.
    397      * If we crash while this operation is in progress, the parent keys can be left uninitialized.
    398      * @param db
    399      */
    400     private void fixParentKeys(SQLiteDatabase db) {
    401         LogUtils.d(TAG, "Fixing parent keys");
    402 
    403         // Update the parentKey for each mailbox row to match the _id of the row whose
    404         // serverId matches our parentServerId. This will leave parentKey blank for any
    405         // row that does not have a parentServerId
    406 
    407         // This is kind of a confusing sql statement, so here's the actual text of it,
    408         // for reference:
    409         //
    410         //   update mailbox set parentKey = (select _id from mailbox as b where
    411         //   mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and
    412         //   mailbox.accountKey=b.accountKey)
    413         db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "="
    414                 + "(select " + Mailbox.ID + " from " + Mailbox.TABLE_NAME + " as b where "
    415                 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "="
    416                 + "b." + MailboxColumns.SERVER_ID + " and "
    417                 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and "
    418                 + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY
    419                 + "=b." + Mailbox.ACCOUNT_KEY + ")");
    420 
    421         // Top level folders can still have uninitialized parent keys. Update these
    422         // to indicate that the parent is -1.
    423         //
    424         //   update mailbox set parentKey = -1 where parentKey=0 or parentKey is null;
    425         db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY
    426                 + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY
    427                 + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY
    428                 + " is null");
    429 
    430     }
    431 
    432     // exposed for testing
    433     public SQLiteDatabase getDatabase(Context context) {
    434         synchronized (sDatabaseLock) {
    435             // Always return the cached database, if we've got one
    436             if (mDatabase != null) {
    437                 return mDatabase;
    438             }
    439 
    440             // Whenever we create or re-cache the databases, make sure that we haven't lost one
    441             // to corruption
    442             checkDatabases();
    443 
    444             DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
    445             mDatabase = helper.getWritableDatabase();
    446             DBHelper.BodyDatabaseHelper bodyHelper =
    447                     new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME);
    448             mBodyDatabase = bodyHelper.getWritableDatabase();
    449             if (mBodyDatabase != null) {
    450                 String bodyFileName = mBodyDatabase.getPath();
    451                 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
    452             }
    453 
    454             // Restore accounts if the database is corrupted...
    455             restoreIfNeeded(context, mDatabase);
    456             // Check for any orphaned Messages in the updated/deleted tables
    457             deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
    458             deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME);
    459             // Delete orphaned mailboxes/messages/policies (account no longer exists)
    460             deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY,
    461                     AccountColumns.ID, Account.TABLE_NAME);
    462             deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY,
    463                     AccountColumns.ID, Account.TABLE_NAME);
    464             deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID,
    465                     AccountColumns.POLICY_KEY, Account.TABLE_NAME);
    466             fixParentKeys(mDatabase);
    467             initUiProvider();
    468             return mDatabase;
    469         }
    470     }
    471 
    472     /**
    473      * Perform startup actions related to UI
    474      */
    475     private void initUiProvider() {
    476         // Clear mailbox sync status
    477         mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS +
    478                 "=" + UIProvider.SyncStatus.NO_SYNC);
    479     }
    480 
    481     /**
    482      * Restore user Account and HostAuth data from our backup database
    483      */
    484     private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
    485         if (MailActivityEmail.DEBUG) {
    486             LogUtils.w(TAG, "restoreIfNeeded...");
    487         }
    488         // Check for legacy backup
    489         String legacyBackup = Preferences.getLegacyBackupPreference(context);
    490         // If there's a legacy backup, create a new-style backup and delete the legacy backup
    491         // In the 1:1000000000 chance that the user gets an app update just as his database becomes
    492         // corrupt, oh well...
    493         if (!TextUtils.isEmpty(legacyBackup)) {
    494             backupAccounts(context, mainDatabase);
    495             Preferences.clearLegacyBackupPreference(context);
    496             LogUtils.w(TAG, "Created new EmailProvider backup database");
    497             return;
    498         }
    499 
    500         // If we have accounts, we're done
    501         if (DatabaseUtils.longForQuery(mainDatabase,
    502                                       "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )",
    503                                       EmailContent.ID_PROJECTION) > 0) {
    504           if (MailActivityEmail.DEBUG) {
    505               LogUtils.w(TAG, "restoreIfNeeded: Account exists.");
    506           }
    507           return;
    508         }
    509 
    510         restoreAccounts(context, mainDatabase);
    511     }
    512 
    513     /** {@inheritDoc} */
    514     @Override
    515     public void shutdown() {
    516         if (mDatabase != null) {
    517             mDatabase.close();
    518             mDatabase = null;
    519         }
    520         if (mBodyDatabase != null) {
    521             mBodyDatabase.close();
    522             mBodyDatabase = null;
    523         }
    524     }
    525 
    526     // exposed for testing
    527     public static void deleteMessageOrphans(SQLiteDatabase database, String tableName) {
    528         if (database != null) {
    529             // We'll look at all of the items in the table; there won't be many typically
    530             Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
    531             // Usually, there will be nothing in these tables, so make a quick check
    532             try {
    533                 if (c.getCount() == 0) return;
    534                 ArrayList<Long> foundMailboxes = new ArrayList<Long>();
    535                 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
    536                 ArrayList<Long> deleteList = new ArrayList<Long>();
    537                 String[] bindArray = new String[1];
    538                 while (c.moveToNext()) {
    539                     // Get the mailbox key and see if we've already found this mailbox
    540                     // If so, we're fine
    541                     long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
    542                     // If we already know this mailbox doesn't exist, mark the message for deletion
    543                     if (notFoundMailboxes.contains(mailboxId)) {
    544                         deleteList.add(c.getLong(ORPHANS_ID));
    545                     // If we don't know about this mailbox, we'll try to find it
    546                     } else if (!foundMailboxes.contains(mailboxId)) {
    547                         bindArray[0] = Long.toString(mailboxId);
    548                         Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
    549                                 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
    550                         try {
    551                             // If it exists, we'll add it to the "found" mailboxes
    552                             if (boxCursor.moveToFirst()) {
    553                                 foundMailboxes.add(mailboxId);
    554                             // Otherwise, we'll add to "not found" and mark the message for deletion
    555                             } else {
    556                                 notFoundMailboxes.add(mailboxId);
    557                                 deleteList.add(c.getLong(ORPHANS_ID));
    558                             }
    559                         } finally {
    560                             boxCursor.close();
    561                         }
    562                     }
    563                 }
    564                 // Now, delete the orphan messages
    565                 for (long messageId: deleteList) {
    566                     bindArray[0] = Long.toString(messageId);
    567                     database.delete(tableName, WHERE_ID, bindArray);
    568                 }
    569             } finally {
    570                 c.close();
    571             }
    572         }
    573     }
    574 
    575     @Override
    576     public int delete(Uri uri, String selection, String[] selectionArgs) {
    577         Log.d(TAG, "Delete: " + uri);
    578         final int match = findMatch(uri, "delete");
    579         Context context = getContext();
    580         // Pick the correct database for this operation
    581         // If we're in a transaction already (which would happen during applyBatch), then the
    582         // body database is already attached to the email database and any attempt to use the
    583         // body database directly will result in a SQLiteException (the database is locked)
    584         SQLiteDatabase db = getDatabase(context);
    585         int table = match >> BASE_SHIFT;
    586         String id = "0";
    587         boolean messageDeletion = false;
    588         ContentResolver resolver = context.getContentResolver();
    589 
    590         String tableName = TABLE_NAMES.valueAt(table);
    591         int result = -1;
    592 
    593         try {
    594             if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
    595                 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    596                     notifyUIConversation(uri);
    597                 }
    598             }
    599             switch (match) {
    600                 case UI_MESSAGE:
    601                     return uiDeleteMessage(uri);
    602                 case UI_ACCOUNT_DATA:
    603                     return uiDeleteAccountData(uri);
    604                 case UI_ACCOUNT:
    605                     return uiDeleteAccount(uri);
    606                 case MESSAGE_SELECTION:
    607                     Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
    608                             selectionArgs, null, null, null);
    609                     try {
    610                         if (findCursor.moveToFirst()) {
    611                             return delete(ContentUris.withAppendedId(
    612                                     Message.CONTENT_URI,
    613                                     findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
    614                                     null, null);
    615                         } else {
    616                             return 0;
    617                         }
    618                     } finally {
    619                         findCursor.close();
    620                     }
    621                 // These are cases in which one or more Messages might get deleted, either by
    622                 // cascade or explicitly
    623                 case MAILBOX_ID:
    624                 case MAILBOX:
    625                 case ACCOUNT_ID:
    626                 case ACCOUNT:
    627                 case MESSAGE:
    628                 case SYNCED_MESSAGE_ID:
    629                 case MESSAGE_ID:
    630                     // Handle lost Body records here, since this cannot be done in a trigger
    631                     // The process is:
    632                     //  1) Begin a transaction, ensuring that both databases are affected atomically
    633                     //  2) Do the requested deletion, with cascading deletions handled in triggers
    634                     //  3) End the transaction, committing all changes atomically
    635                     //
    636                     // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
    637                     messageDeletion = true;
    638                     db.beginTransaction();
    639                     break;
    640             }
    641             switch (match) {
    642                 case BODY_ID:
    643                 case DELETED_MESSAGE_ID:
    644                 case SYNCED_MESSAGE_ID:
    645                 case MESSAGE_ID:
    646                 case UPDATED_MESSAGE_ID:
    647                 case ATTACHMENT_ID:
    648                 case MAILBOX_ID:
    649                 case ACCOUNT_ID:
    650                 case HOSTAUTH_ID:
    651                 case POLICY_ID:
    652                 case QUICK_RESPONSE_ID:
    653                     id = uri.getPathSegments().get(1);
    654                     if (match == SYNCED_MESSAGE_ID) {
    655                         // For synced messages, first copy the old message to the deleted table and
    656                         // delete it from the updated table (in case it was updated first)
    657                         // Note that this is all within a transaction, for atomicity
    658                         db.execSQL(DELETED_MESSAGE_INSERT + id);
    659                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
    660                     }
    661 
    662                     final long accountId;
    663                     if (match == MAILBOX_ID) {
    664                         accountId = Mailbox.getAccountIdForMailbox(context, id);
    665                     } else {
    666                         accountId = Account.NO_ACCOUNT;
    667                     }
    668 
    669                     result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
    670 
    671                     if (match == ACCOUNT_ID) {
    672                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
    673                         resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
    674                     } else if (match == MAILBOX_ID) {
    675                         notifyUIFolder(id, accountId);
    676                     } else if (match == ATTACHMENT_ID) {
    677                         notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
    678                     }
    679                     break;
    680                 case ATTACHMENTS_MESSAGE_ID:
    681                     // All attachments for the given message
    682                     id = uri.getPathSegments().get(2);
    683                     result = db.delete(tableName,
    684                             whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
    685                     break;
    686 
    687                 case BODY:
    688                 case MESSAGE:
    689                 case DELETED_MESSAGE:
    690                 case UPDATED_MESSAGE:
    691                 case ATTACHMENT:
    692                 case MAILBOX:
    693                 case ACCOUNT:
    694                 case HOSTAUTH:
    695                 case POLICY:
    696                     result = db.delete(tableName, selection, selectionArgs);
    697                     break;
    698                 case MESSAGE_MOVE:
    699                     db.delete(MessageMove.TABLE_NAME, selection, selectionArgs);
    700                     break;
    701                 case MESSAGE_STATE_CHANGE:
    702                     db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs);
    703                     break;
    704                 default:
    705                     throw new IllegalArgumentException("Unknown URI " + uri);
    706             }
    707             if (messageDeletion) {
    708                 if (match == MESSAGE_ID) {
    709                     // Delete the Body record associated with the deleted message
    710                     db.execSQL(DELETE_BODY + id);
    711                 } else {
    712                     // Delete any orphaned Body records
    713                     db.execSQL(DELETE_ORPHAN_BODIES);
    714                 }
    715                 db.setTransactionSuccessful();
    716             }
    717         } catch (SQLiteException e) {
    718             checkDatabases();
    719             throw e;
    720         } finally {
    721             if (messageDeletion) {
    722                 db.endTransaction();
    723             }
    724         }
    725 
    726         // Notify all notifier cursors
    727         sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
    728 
    729         // Notify all email content cursors
    730         resolver.notifyChange(EmailContent.CONTENT_URI, null);
    731         return result;
    732     }
    733 
    734     @Override
    735     // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
    736     public String getType(Uri uri) {
    737         int match = findMatch(uri, "getType");
    738         switch (match) {
    739             case BODY_ID:
    740                 return "vnd.android.cursor.item/email-body";
    741             case BODY:
    742                 return "vnd.android.cursor.dir/email-body";
    743             case UPDATED_MESSAGE_ID:
    744             case MESSAGE_ID:
    745                 // NOTE: According to the framework folks, we're supposed to invent mime types as
    746                 // a way of passing information to drag & drop recipients.
    747                 // If there's a mailboxId parameter in the url, we respond with a mime type that
    748                 // has -n appended, where n is the mailboxId of the message.  The drag & drop code
    749                 // uses this information to know not to allow dragging the item to its own mailbox
    750                 String mimeType = EMAIL_MESSAGE_MIME_TYPE;
    751                 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID);
    752                 if (mailboxId != null) {
    753                     mimeType += "-" + mailboxId;
    754                 }
    755                 return mimeType;
    756             case UPDATED_MESSAGE:
    757             case MESSAGE:
    758                 return "vnd.android.cursor.dir/email-message";
    759             case MAILBOX:
    760                 return "vnd.android.cursor.dir/email-mailbox";
    761             case MAILBOX_ID:
    762                 return "vnd.android.cursor.item/email-mailbox";
    763             case ACCOUNT:
    764                 return "vnd.android.cursor.dir/email-account";
    765             case ACCOUNT_ID:
    766                 return "vnd.android.cursor.item/email-account";
    767             case ATTACHMENTS_MESSAGE_ID:
    768             case ATTACHMENT:
    769                 return "vnd.android.cursor.dir/email-attachment";
    770             case ATTACHMENT_ID:
    771                 return EMAIL_ATTACHMENT_MIME_TYPE;
    772             case HOSTAUTH:
    773                 return "vnd.android.cursor.dir/email-hostauth";
    774             case HOSTAUTH_ID:
    775                 return "vnd.android.cursor.item/email-hostauth";
    776             default:
    777                 return null;
    778         }
    779     }
    780 
    781     // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI
    782     // as the base because that gets spammed.
    783     // These can't be statically initialized because they depend on EmailContent.AUTHORITY
    784     private static Uri UIPROVIDER_CONVERSATION_NOTIFIER;
    785     private static Uri UIPROVIDER_FOLDER_NOTIFIER;
    786     private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER;
    787     private static Uri UIPROVIDER_ACCOUNT_NOTIFIER;
    788     // Not currently used
    789     //public static Uri UIPROVIDER_SETTINGS_NOTIFIER;
    790     private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER;
    791     private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER;
    792     public static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER;
    793     private static Uri UIPROVIDER_MESSAGE_NOTIFIER;
    794     private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER;
    795 
    796     @Override
    797     public Uri insert(Uri uri, ContentValues values) {
    798         Log.d(TAG, "Insert: " + uri);
    799         int match = findMatch(uri, "insert");
    800         Context context = getContext();
    801         ContentResolver resolver = context.getContentResolver();
    802 
    803         // See the comment at delete(), above
    804         SQLiteDatabase db = getDatabase(context);
    805         int table = match >> BASE_SHIFT;
    806         String id = "0";
    807         long longId;
    808 
    809         // We do NOT allow setting of unreadCount/messageCount via the provider
    810         // These columns are maintained via triggers
    811         if (match == MAILBOX_ID || match == MAILBOX) {
    812             values.put(MailboxColumns.UNREAD_COUNT, 0);
    813             values.put(MailboxColumns.MESSAGE_COUNT, 0);
    814         }
    815 
    816         final Uri resultUri;
    817 
    818         try {
    819             switch (match) {
    820                 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
    821                 // or DELETED_MESSAGE; see the comment below for details
    822                 case UPDATED_MESSAGE:
    823                 case DELETED_MESSAGE:
    824                 case MESSAGE:
    825                 case BODY:
    826                 case ATTACHMENT:
    827                 case MAILBOX:
    828                 case ACCOUNT:
    829                 case HOSTAUTH:
    830                 case POLICY:
    831                 case QUICK_RESPONSE:
    832                     longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
    833                     resultUri = ContentUris.withAppendedId(uri, longId);
    834                     switch(match) {
    835                         case MESSAGE:
    836                             final long mailboxId = values.getAsLong(Message.MAILBOX_KEY);
    837                             if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    838                                 notifyUIConversationMailbox(mailboxId);
    839                             }
    840                             notifyUIFolder(mailboxId, values.getAsLong(Message.ACCOUNT_KEY));
    841                             break;
    842                         case MAILBOX:
    843                             if (values.containsKey(MailboxColumns.TYPE)) {
    844                                 if (values.getAsInteger(MailboxColumns.TYPE) <
    845                                         Mailbox.TYPE_NOT_EMAIL) {
    846                                     // Notify the account when a new mailbox is added
    847                                     final Long accountId =
    848                                             values.getAsLong(MailboxColumns.ACCOUNT_KEY);
    849                                     if (accountId != null && accountId > 0) {
    850                                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId);
    851                                         notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
    852                                     }
    853                                 }
    854                             }
    855                             break;
    856                         case ACCOUNT:
    857                             updateAccountSyncInterval(longId, values);
    858                             if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
    859                                 notifyUIAccount(longId);
    860                             }
    861                             resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
    862                             break;
    863                         case UPDATED_MESSAGE:
    864                         case DELETED_MESSAGE:
    865                             throw new IllegalArgumentException("Unknown URL " + uri);
    866                         case ATTACHMENT:
    867                             int flags = 0;
    868                             if (values.containsKey(Attachment.FLAGS)) {
    869                                 flags = values.getAsInteger(Attachment.FLAGS);
    870                             }
    871                             // Report all new attachments to the download service
    872                             if (TextUtils.isEmpty(values.getAsString(Attachment.LOCATION))) {
    873                                 LogUtils.w(TAG, new Throwable(), "attachment with blank location");
    874                             }
    875                             mAttachmentService.attachmentChanged(getContext(), longId, flags);
    876                             break;
    877                     }
    878                     break;
    879                 case QUICK_RESPONSE_ACCOUNT_ID:
    880                     longId = Long.parseLong(uri.getPathSegments().get(2));
    881                     values.put(EmailContent.QuickResponseColumns.ACCOUNT_KEY, longId);
    882                     return insert(QuickResponse.CONTENT_URI, values);
    883                 case MAILBOX_ID:
    884                     // This implies adding a message to a mailbox
    885                     // Hmm, a problem here is that we can't link the account as well, so it must be
    886                     // already in the values...
    887                     longId = Long.parseLong(uri.getPathSegments().get(1));
    888                     values.put(MessageColumns.MAILBOX_KEY, longId);
    889                     return insert(Message.CONTENT_URI, values); // Recurse
    890                 case MESSAGE_ID:
    891                     // This implies adding an attachment to a message.
    892                     id = uri.getPathSegments().get(1);
    893                     longId = Long.parseLong(id);
    894                     values.put(AttachmentColumns.MESSAGE_KEY, longId);
    895                     return insert(Attachment.CONTENT_URI, values); // Recurse
    896                 case ACCOUNT_ID:
    897                     // This implies adding a mailbox to an account.
    898                     longId = Long.parseLong(uri.getPathSegments().get(1));
    899                     values.put(MailboxColumns.ACCOUNT_KEY, longId);
    900                     return insert(Mailbox.CONTENT_URI, values); // Recurse
    901                 case ATTACHMENTS_MESSAGE_ID:
    902                     longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
    903                     resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
    904                     break;
    905                 default:
    906                     throw new IllegalArgumentException("Unknown URL " + uri);
    907             }
    908         } catch (SQLiteException e) {
    909             checkDatabases();
    910             throw e;
    911         }
    912 
    913         // Notify all notifier cursors
    914         sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
    915 
    916         // Notify all existing cursors.
    917         resolver.notifyChange(EmailContent.CONTENT_URI, null);
    918         return resultUri;
    919     }
    920 
    921     @Override
    922     public boolean onCreate() {
    923         Context context = getContext();
    924         EmailContent.init(context);
    925         init(context);
    926         // Do this last, so that EmailContent/EmailProvider are initialized
    927         MailActivityEmail.setServicesEnabledAsync(context);
    928 
    929         // Update widgets
    930         final Intent updateAllWidgetsIntent =
    931                 new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED);
    932         updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true);
    933         updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type));
    934         context.sendBroadcast(updateAllWidgetsIntent);
    935 
    936         // The combined account name changes on locale changes
    937         final Configuration oldConfiguration =
    938                 new Configuration(context.getResources().getConfiguration());
    939         context.registerComponentCallbacks(new ComponentCallbacks() {
    940             @Override
    941             public void onConfigurationChanged(Configuration configuration) {
    942                 int delta = oldConfiguration.updateFrom(configuration);
    943                 if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) {
    944                     notifyUIAccount(COMBINED_ACCOUNT_ID);
    945                 }
    946             }
    947 
    948             @Override
    949             public void onLowMemory() {}
    950         });
    951 
    952         return false;
    953     }
    954 
    955     private static void init(final Context context) {
    956         // Synchronize on the matcher rather than the class object to minimize risk of contention
    957         // & deadlock.
    958         synchronized (sURIMatcher) {
    959             // We use the existence of this variable as indicative of whether this function has
    960             // already run.
    961             if (INTEGRITY_CHECK_URI != null) {
    962                 return;
    963             }
    964             INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY +
    965                     "/integrityCheck");
    966             ACCOUNT_BACKUP_URI =
    967                     Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup");
    968             FOLDER_STATUS_URI =
    969                     Uri.parse("content://" + EmailContent.AUTHORITY + "/status");
    970             EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type);
    971 
    972             final String uiNotificationAuthority =
    973                     EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications";
    974             UIPROVIDER_CONVERSATION_NOTIFIER =
    975                     Uri.parse("content://" + uiNotificationAuthority + "/uimessages");
    976             UIPROVIDER_FOLDER_NOTIFIER =
    977                     Uri.parse("content://" + uiNotificationAuthority + "/uifolder");
    978             UIPROVIDER_FOLDERLIST_NOTIFIER =
    979                     Uri.parse("content://" + uiNotificationAuthority + "/uifolders");
    980             UIPROVIDER_ACCOUNT_NOTIFIER =
    981                     Uri.parse("content://" + uiNotificationAuthority + "/uiaccount");
    982             // Not currently used
    983             /* UIPROVIDER_SETTINGS_NOTIFIER =
    984                     Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/
    985             UIPROVIDER_ATTACHMENT_NOTIFIER =
    986                     Uri.parse("content://" + uiNotificationAuthority + "/uiattachment");
    987             UIPROVIDER_ATTACHMENTS_NOTIFIER =
    988                     Uri.parse("content://" + uiNotificationAuthority + "/uiattachments");
    989             UIPROVIDER_ALL_ACCOUNTS_NOTIFIER =
    990                     Uri.parse("content://" + uiNotificationAuthority + "/uiaccts");
    991             UIPROVIDER_MESSAGE_NOTIFIER =
    992                     Uri.parse("content://" + uiNotificationAuthority + "/uimessage");
    993             UIPROVIDER_RECENT_FOLDERS_NOTIFIER =
    994                     Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders");
    995 
    996 
    997             // All accounts
    998             sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT);
    999             // A specific account
   1000             // insert into this URI causes a mailbox to be added to the account
   1001             sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID);
   1002             sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK);
   1003 
   1004             // Special URI to reset the new message count.  Only update works, and values
   1005             // will be ignored.
   1006             sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount",
   1007                     ACCOUNT_RESET_NEW_COUNT);
   1008             sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#",
   1009                     ACCOUNT_RESET_NEW_COUNT_ID);
   1010 
   1011             // All mailboxes
   1012             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX);
   1013             // A specific mailbox
   1014             // insert into this URI causes a message to be added to the mailbox
   1015             // ** NOTE For now, the accountKey must be set manually in the values!
   1016             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID);
   1017             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#",
   1018                     MAILBOX_NOTIFICATION);
   1019             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#",
   1020                     MAILBOX_MOST_RECENT_MESSAGE);
   1021             sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT);
   1022 
   1023             // All messages
   1024             sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE);
   1025             // A specific message
   1026             // insert into this URI causes an attachment to be added to the message
   1027             sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID);
   1028 
   1029             // A specific attachment
   1030             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT);
   1031             // A specific attachment (the header information)
   1032             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID);
   1033             // The attachments of a specific message (query only) (insert & delete TBD)
   1034             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#",
   1035                     ATTACHMENTS_MESSAGE_ID);
   1036             sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile",
   1037                     ATTACHMENTS_CACHED_FILE_ACCESS);
   1038 
   1039             // All mail bodies
   1040             sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
   1041             // A specific mail body
   1042             sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
   1043 
   1044             // All hostauth records
   1045             sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
   1046             // A specific hostauth
   1047             sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID);
   1048 
   1049             /**
   1050              * THIS URI HAS SPECIAL SEMANTICS
   1051              * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK
   1052              * TO A SERVER VIA A SYNC ADAPTER
   1053              */
   1054             sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
   1055             sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION);
   1056 
   1057             sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE);
   1058             sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH,
   1059                     MESSAGE_STATE_CHANGE);
   1060 
   1061             /**
   1062              * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
   1063              * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
   1064              * BY THE UI APPLICATION
   1065              */
   1066             // All deleted messages
   1067             sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE);
   1068             // A specific deleted message
   1069             sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
   1070 
   1071             // All updated messages
   1072             sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
   1073             // A specific updated message
   1074             sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
   1075 
   1076             CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues();
   1077             CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0);
   1078 
   1079             sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY);
   1080             sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID);
   1081 
   1082             // All quick responses
   1083             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE);
   1084             // A specific quick response
   1085             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID);
   1086             // All quick responses associated with a particular account id
   1087             sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#",
   1088                     QUICK_RESPONSE_ACCOUNT_ID);
   1089 
   1090             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS);
   1091             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS);
   1092             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS);
   1093             sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS);
   1094             sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES);
   1095             sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE);
   1096             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO);
   1097             sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH);
   1098             // We listen to everything trailing uifolder/ since there might be an appVersion
   1099             // as in Utils.appendVersionQueryParameter().
   1100             sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER);
   1101             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT);
   1102             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS);
   1103             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS);
   1104             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT);
   1105             sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
   1106             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
   1107             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
   1108             sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
   1109             sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS);
   1110             sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
   1111                     UI_DEFAULT_RECENT_FOLDERS);
   1112             sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#",
   1113                     ACCOUNT_PICK_TRASH_FOLDER);
   1114             sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#",
   1115                     ACCOUNT_PICK_SENT_FOLDER);
   1116         }
   1117     }
   1118 
   1119     /**
   1120      * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
   1121      * always be in sync (i.e. there are two database or NO databases).  This code will delete
   1122      * any "orphan" database, so that both will be created together.  Note that an "orphan" database
   1123      * will exist after either of the individual databases is deleted due to data corruption.
   1124      */
   1125     public void checkDatabases() {
   1126         synchronized (sDatabaseLock) {
   1127             // Uncache the databases
   1128             if (mDatabase != null) {
   1129                 mDatabase = null;
   1130             }
   1131             if (mBodyDatabase != null) {
   1132                 mBodyDatabase = null;
   1133             }
   1134             // Look for orphans, and delete as necessary; these must always be in sync
   1135             final File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
   1136             final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
   1137 
   1138             // TODO Make sure attachments are deleted
   1139             if (databaseFile.exists() && !bodyFile.exists()) {
   1140                 LogUtils.w(TAG, "Deleting orphaned EmailProvider database...");
   1141                 getContext().deleteDatabase(DATABASE_NAME);
   1142             } else if (bodyFile.exists() && !databaseFile.exists()) {
   1143                 LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database...");
   1144                 getContext().deleteDatabase(BODY_DATABASE_NAME);
   1145             }
   1146         }
   1147     }
   1148 
   1149     @Override
   1150     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   1151             String sortOrder) {
   1152         Cursor c = null;
   1153         int match;
   1154         try {
   1155             match = findMatch(uri, "query");
   1156         } catch (IllegalArgumentException e) {
   1157             String uriString = uri.toString();
   1158             // If we were passed an illegal uri, see if it ends in /-1
   1159             // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
   1160             if (uriString != null && uriString.endsWith("/-1")) {
   1161                 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
   1162                 match = findMatch(uri, "query");
   1163                 switch (match) {
   1164                     case BODY_ID:
   1165                     case MESSAGE_ID:
   1166                     case DELETED_MESSAGE_ID:
   1167                     case UPDATED_MESSAGE_ID:
   1168                     case ATTACHMENT_ID:
   1169                     case MAILBOX_ID:
   1170                     case ACCOUNT_ID:
   1171                     case HOSTAUTH_ID:
   1172                     case POLICY_ID:
   1173                         return new MatrixCursorWithCachedColumns(projection, 0);
   1174                 }
   1175             }
   1176             throw e;
   1177         }
   1178         Context context = getContext();
   1179         // See the comment at delete(), above
   1180         SQLiteDatabase db = getDatabase(context);
   1181         int table = match >> BASE_SHIFT;
   1182         String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
   1183         String id;
   1184 
   1185         String tableName = TABLE_NAMES.valueAt(table);
   1186 
   1187         try {
   1188             switch (match) {
   1189                 // First, dispatch queries from UnfiedEmail
   1190                 case UI_SEARCH:
   1191                     c = uiSearch(uri, projection);
   1192                     return c;
   1193                 case UI_ACCTS:
   1194                     c = uiAccounts(projection);
   1195                     return c;
   1196                 case UI_UNDO:
   1197                     return uiUndo(projection);
   1198                 case UI_SUBFOLDERS:
   1199                 case UI_MESSAGES:
   1200                 case UI_MESSAGE:
   1201                 case UI_FOLDER:
   1202                 case UI_ACCOUNT:
   1203                 case UI_ATTACHMENT:
   1204                 case UI_ATTACHMENTS:
   1205                 case UI_CONVERSATION:
   1206                 case UI_RECENT_FOLDERS:
   1207                 case UI_FULL_FOLDERS:
   1208                 case UI_ALL_FOLDERS:
   1209                     // For now, we don't allow selection criteria within these queries
   1210                     if (selection != null || selectionArgs != null) {
   1211                         throw new IllegalArgumentException("UI queries can't have selection/args");
   1212                     }
   1213 
   1214                     final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER);
   1215                     final boolean unseenOnly =
   1216                             seenParam != null && Boolean.FALSE.toString().equals(seenParam);
   1217 
   1218                     c = uiQuery(match, uri, projection, unseenOnly);
   1219                     return c;
   1220                 case UI_FOLDERS:
   1221                     c = uiFolders(uri, projection);
   1222                     return c;
   1223                 case UI_FOLDER_LOAD_MORE:
   1224                     c = uiFolderLoadMore(getMailbox(uri));
   1225                     return c;
   1226                 case UI_FOLDER_REFRESH:
   1227                     c = uiFolderRefresh(getMailbox(uri), 0);
   1228                     return c;
   1229                 case MAILBOX_NOTIFICATION:
   1230                     c = notificationQuery(uri);
   1231                     return c;
   1232                 case MAILBOX_MOST_RECENT_MESSAGE:
   1233                     c = mostRecentMessageQuery(uri);
   1234                     return c;
   1235                 case MAILBOX_MESSAGE_COUNT:
   1236                     c = getMailboxMessageCount(uri);
   1237                     return c;
   1238                 case MESSAGE_MOVE:
   1239                     return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs,
   1240                             null, null, sortOrder, limit);
   1241                 case MESSAGE_STATE_CHANGE:
   1242                     return db.query(MessageStateChange.TABLE_NAME, projection, selection,
   1243                             selectionArgs, null, null, sortOrder, limit);
   1244                 case BODY:
   1245                 case MESSAGE:
   1246                 case UPDATED_MESSAGE:
   1247                 case DELETED_MESSAGE:
   1248                 case ATTACHMENT:
   1249                 case MAILBOX:
   1250                 case ACCOUNT:
   1251                 case HOSTAUTH:
   1252                 case POLICY:
   1253                     c = db.query(tableName, projection,
   1254                             selection, selectionArgs, null, null, sortOrder, limit);
   1255                     break;
   1256                 case QUICK_RESPONSE:
   1257                     c = uiQuickResponse(projection);
   1258                     break;
   1259                 case BODY_ID:
   1260                 case MESSAGE_ID:
   1261                 case DELETED_MESSAGE_ID:
   1262                 case UPDATED_MESSAGE_ID:
   1263                 case ATTACHMENT_ID:
   1264                 case MAILBOX_ID:
   1265                 case ACCOUNT_ID:
   1266                 case HOSTAUTH_ID:
   1267                 case POLICY_ID:
   1268                     id = uri.getPathSegments().get(1);
   1269                     c = db.query(tableName, projection, whereWithId(id, selection),
   1270                             selectionArgs, null, null, sortOrder, limit);
   1271                     break;
   1272                 case QUICK_RESPONSE_ID:
   1273                     id = uri.getPathSegments().get(1);
   1274                     c = uiQuickResponseId(projection, id);
   1275                     break;
   1276                 case ATTACHMENTS_MESSAGE_ID:
   1277                     // All attachments for the given message
   1278                     id = uri.getPathSegments().get(2);
   1279                     c = db.query(Attachment.TABLE_NAME, projection,
   1280                             whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
   1281                             selectionArgs, null, null, sortOrder, limit);
   1282                     break;
   1283                 case QUICK_RESPONSE_ACCOUNT_ID:
   1284                     // All quick responses for the given account
   1285                     id = uri.getPathSegments().get(2);
   1286                     c = uiQuickResponseAccount(projection, id);
   1287                     break;
   1288                 default:
   1289                     throw new IllegalArgumentException("Unknown URI " + uri);
   1290             }
   1291         } catch (SQLiteException e) {
   1292             checkDatabases();
   1293             throw e;
   1294         } catch (RuntimeException e) {
   1295             checkDatabases();
   1296             e.printStackTrace();
   1297             throw e;
   1298         } finally {
   1299             if (c == null) {
   1300                 // This should never happen, but let's be sure to log it...
   1301                 // TODO: There are actually cases where c == null is expected, for example
   1302                 // UI_FOLDER_LOAD_MORE.
   1303                 // Demoting this to a warning for now until we figure out what to do with it.
   1304                 LogUtils.w(TAG, "Query returning null for uri: " + uri + ", selection: "
   1305                         + selection);
   1306             }
   1307         }
   1308 
   1309         if ((c != null) && !isTemporary()) {
   1310             c.setNotificationUri(getContext().getContentResolver(), uri);
   1311         }
   1312         return c;
   1313     }
   1314 
   1315     private static String whereWithId(String id, String selection) {
   1316         StringBuilder sb = new StringBuilder(256);
   1317         sb.append("_id=");
   1318         sb.append(id);
   1319         if (selection != null) {
   1320             sb.append(" AND (");
   1321             sb.append(selection);
   1322             sb.append(')');
   1323         }
   1324         return sb.toString();
   1325     }
   1326 
   1327     /**
   1328      * Combine a locally-generated selection with a user-provided selection
   1329      *
   1330      * This introduces risk that the local selection might insert incorrect chars
   1331      * into the SQL, so use caution.
   1332      *
   1333      * @param where locally-generated selection, must not be null
   1334      * @param selection user-provided selection, may be null
   1335      * @return a single selection string
   1336      */
   1337     private static String whereWith(String where, String selection) {
   1338         if (selection == null) {
   1339             return where;
   1340         }
   1341         StringBuilder sb = new StringBuilder(where);
   1342         sb.append(" AND (");
   1343         sb.append(selection);
   1344         sb.append(')');
   1345 
   1346         return sb.toString();
   1347     }
   1348 
   1349     /**
   1350      * Restore a HostAuth from a database, given its unique id
   1351      * @param db the database
   1352      * @param id the unique id (_id) of the row
   1353      * @return a fully populated HostAuth or null if the row does not exist
   1354      */
   1355     private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
   1356         Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
   1357                 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null);
   1358         try {
   1359             if (c.moveToFirst()) {
   1360                 HostAuth hostAuth = new HostAuth();
   1361                 hostAuth.restore(c);
   1362                 return hostAuth;
   1363             }
   1364             return null;
   1365         } finally {
   1366             c.close();
   1367         }
   1368     }
   1369 
   1370     /**
   1371      * Copy the Account and HostAuth tables from one database to another
   1372      * @param fromDatabase the source database
   1373      * @param toDatabase the destination database
   1374      * @return the number of accounts copied, or -1 if an error occurred
   1375      */
   1376     private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
   1377         if (fromDatabase == null || toDatabase == null) return -1;
   1378 
   1379         // Lock both databases; for the "from" database, we don't want anyone changing it from
   1380         // under us; for the "to" database, we want to make the operation atomic
   1381         int copyCount = 0;
   1382         fromDatabase.beginTransaction();
   1383         try {
   1384             toDatabase.beginTransaction();
   1385             try {
   1386                 // Delete anything hanging around here
   1387                 toDatabase.delete(Account.TABLE_NAME, null, null);
   1388                 toDatabase.delete(HostAuth.TABLE_NAME, null, null);
   1389 
   1390                 // Get our account cursor
   1391                 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
   1392                         null, null, null, null, null);
   1393                 if (c == null) return 0;
   1394                 LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount());
   1395                 try {
   1396                     // Loop through accounts, copying them and associated host auth's
   1397                     while (c.moveToNext()) {
   1398                         Account account = new Account();
   1399                         account.restore(c);
   1400 
   1401                         // Clear security sync key and sync key, as these were specific to the
   1402                         // state of the account, and we've reset that...
   1403                         // Clear policy key so that we can re-establish policies from the server
   1404                         // TODO This is pretty EAS specific, but there's a lot of that around
   1405                         account.mSecuritySyncKey = null;
   1406                         account.mSyncKey = null;
   1407                         account.mPolicyKey = 0;
   1408 
   1409                         // Copy host auth's and update foreign keys
   1410                         HostAuth hostAuth = restoreHostAuth(fromDatabase,
   1411                                 account.mHostAuthKeyRecv);
   1412 
   1413                         // The account might have gone away, though very unlikely
   1414                         if (hostAuth == null) continue;
   1415                         account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
   1416                                 hostAuth.toContentValues());
   1417 
   1418                         // EAS accounts have no send HostAuth
   1419                         if (account.mHostAuthKeySend > 0) {
   1420                             hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
   1421                             // Belt and suspenders; I can't imagine that this is possible,
   1422                             // since we checked the validity of the account above, and the
   1423                             // database is now locked
   1424                             if (hostAuth == null) continue;
   1425                             account.mHostAuthKeySend = toDatabase.insert(
   1426                                     HostAuth.TABLE_NAME, null, hostAuth.toContentValues());
   1427                         }
   1428 
   1429                         // Now, create the account in the "to" database
   1430                         toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
   1431                         copyCount++;
   1432                     }
   1433                 } finally {
   1434                     c.close();
   1435                 }
   1436 
   1437                 // Say it's ok to commit
   1438                 toDatabase.setTransactionSuccessful();
   1439             } finally {
   1440                 toDatabase.endTransaction();
   1441             }
   1442         } catch (SQLiteException ex) {
   1443             LogUtils.w(TAG, "Exception while copying account tables", ex);
   1444             copyCount = -1;
   1445         } finally {
   1446             fromDatabase.endTransaction();
   1447         }
   1448         return copyCount;
   1449     }
   1450 
   1451     private static SQLiteDatabase getBackupDatabase(Context context) {
   1452         DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME);
   1453         return helper.getWritableDatabase();
   1454     }
   1455 
   1456     /**
   1457      * Backup account data, returning the number of accounts backed up
   1458      */
   1459     private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
   1460         if (MailActivityEmail.DEBUG) {
   1461             LogUtils.d(TAG, "backupAccounts...");
   1462         }
   1463         SQLiteDatabase backupDatabase = getBackupDatabase(context);
   1464         try {
   1465             int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
   1466             if (numBackedUp < 0) {
   1467                 LogUtils.e(TAG, "Account backup failed!");
   1468             } else if (MailActivityEmail.DEBUG) {
   1469                 LogUtils.d(TAG, "Backed up " + numBackedUp + " accounts...");
   1470             }
   1471             return numBackedUp;
   1472         } finally {
   1473             if (backupDatabase != null) {
   1474                 backupDatabase.close();
   1475             }
   1476         }
   1477     }
   1478 
   1479     /**
   1480      * Restore account data, returning the number of accounts restored
   1481      */
   1482     private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
   1483         if (MailActivityEmail.DEBUG) {
   1484             LogUtils.d(TAG, "restoreAccounts...");
   1485         }
   1486         SQLiteDatabase backupDatabase = getBackupDatabase(context);
   1487         try {
   1488             int numRecovered = copyAccountTables(backupDatabase, mainDatabase);
   1489             if (numRecovered > 0) {
   1490                 LogUtils.e(TAG, "Recovered " + numRecovered + " accounts!");
   1491             } else if (numRecovered < 0) {
   1492                 LogUtils.e(TAG, "Account recovery failed?");
   1493             } else if (MailActivityEmail.DEBUG) {
   1494                 LogUtils.d(TAG, "No accounts to restore...");
   1495             }
   1496             return numRecovered;
   1497         } finally {
   1498             if (backupDatabase != null) {
   1499                 backupDatabase.close();
   1500             }
   1501         }
   1502     }
   1503 
   1504     private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s ("
   1505             + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + ","
   1506             + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ",";
   1507 
   1508     private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, "
   1509             + "(select " + Message.SERVER_ID + " from " + Message.TABLE_NAME + " where _id=%s),"
   1510             + "(select " + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
   1511             + MessageMove.STATUS_NONE_STRING + ",";
   1512 
   1513     /**
   1514      * Formatting string to generate the SQL statement for inserting into MessageMove.
   1515      * The formatting parameters are:
   1516      * table name, message id x 4, destination folder id, message id, destination folder id.
   1517      * Duplications are needed for sub-selects.
   1518      */
   1519     private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
   1520             + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + ","
   1521             + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID
   1522             + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
   1523             + "(select " + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
   1524             + "%d,"
   1525             + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select "
   1526             + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)),"
   1527             + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))";
   1528 
   1529     /**
   1530      * Insert a row into the MessageMove table when that message is moved.
   1531      * @param db The {@link SQLiteDatabase}.
   1532      * @param messageId The id of the message being moved.
   1533      * @param dstFolderKey The folder to which the message is being moved.
   1534      */
   1535     private void addToMessageMove(final SQLiteDatabase db, final String messageId,
   1536             final long dstFolderKey) {
   1537         db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME,
   1538                 messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey));
   1539     }
   1540 
   1541     /**
   1542      * Formatting string to generate the SQL statement for inserting into MessageStateChange.
   1543      * The formatting parameters are:
   1544      * table name, message id x 4, new flag read, message id, new flag favorite.
   1545      * Duplications are needed for sub-selects.
   1546      */
   1547     private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
   1548             + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + ","
   1549             + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE
   1550             + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
   1551             + "(select " + Message.FLAG_READ + " from " + Message.TABLE_NAME + " where _id=%s),"
   1552             + "%d,"
   1553             + "(select " + Message.FLAG_FAVORITE + " from " + Message.TABLE_NAME + " where _id=%s),"
   1554             + "%d)";
   1555 
   1556     private void addToMessageStateChange(final SQLiteDatabase db, final String messageId,
   1557             final int newFlagRead, final int newFlagFavorite) {
   1558         db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT,
   1559                 MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId,
   1560                 newFlagRead, messageId, newFlagFavorite));
   1561     }
   1562 
   1563     // select count(*) from (select count(*) as dupes from Mailbox where accountKey=?
   1564     // group by serverId) where dupes > 1;
   1565     private static final String ACCOUNT_INTEGRITY_SQL =
   1566             "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME +
   1567             " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1";
   1568 
   1569 
   1570     // Query to get the protocol for a message. Temporary to switch between new and old upsync
   1571     // behavior; should go away when IMAP gets converted.
   1572     private static final String GET_MESSAGE_DETAILS = "SELECT"
   1573             + " h." + HostAuthColumns.PROTOCOL + ","
   1574             + " m." + Message.MAILBOX_KEY + ","
   1575             + " a." + AccountColumns.ID
   1576             + " FROM " + Message.TABLE_NAME + " AS m"
   1577             + " INNER JOIN " + Account.TABLE_NAME + " AS a"
   1578             + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns.ID
   1579             + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
   1580             + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns.ID
   1581             + " WHERE m." + MessageColumns.ID + "=?";
   1582     private static final int INDEX_PROTOCOL = 0;
   1583     private static final int INDEX_MAILBOX_KEY = 1;
   1584     private static final int INDEX_ACCOUNT_KEY = 2;
   1585 
   1586     /**
   1587      * Query to get the protocol and email address for an account. Note that this uses
   1588      * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns.
   1589      */
   1590     private static final String GET_ACCOUNT_DETAILS = "SELECT"
   1591             + " h." + HostAuthColumns.PROTOCOL + ","
   1592             + " a." + AccountColumns.EMAIL_ADDRESS + ","
   1593             + " a." + AccountColumns.SYNC_KEY
   1594             + " FROM " + Account.TABLE_NAME + " AS a"
   1595             + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
   1596             + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns.ID
   1597             + " WHERE a." + AccountColumns.ID + "=?";
   1598     private static final int INDEX_EMAIL_ADDRESS = 1;
   1599     private static final int INDEX_SYNC_KEY = 2;
   1600 
   1601     /**
   1602      * Restart push if we need it (currently only for Exchange accounts).
   1603      * @param context A {@link Context}.
   1604      * @param db The {@link SQLiteDatabase}.
   1605      * @param id The id of the thing we're looking for.
   1606      * @return Whether or not we sent a request to restart the push.
   1607      */
   1608     private static boolean restartPush(final Context context, final SQLiteDatabase db,
   1609             final String id) {
   1610         final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id});
   1611         if (c != null) {
   1612             try {
   1613                 if (c.moveToFirst()) {
   1614                     final String protocol = c.getString(INDEX_PROTOCOL);
   1615                     // Only restart push for EAS accounts that have completed initial sync.
   1616                     if (context.getString(R.string.protocol_eas).equals(protocol) &&
   1617                             !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) {
   1618                         final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
   1619                         final android.accounts.Account account =
   1620                                 getAccountManagerAccount(context, emailAddress, protocol);
   1621                         restartPush(account);
   1622                         return true;
   1623                     }
   1624                 }
   1625             } finally {
   1626                 c.close();
   1627             }
   1628         }
   1629         return false;
   1630     }
   1631 
   1632     /**
   1633      * Restart push if a mailbox's settings change in a way that requires it.
   1634      * @param context A {@link Context}.
   1635      * @param db The {@link SQLiteDatabase}.
   1636      * @param values The {@link ContentValues} that were updated for the mailbox.
   1637      * @param accountId The id of the account for this mailbox.
   1638      * @return Whether or not the push was restarted.
   1639      */
   1640     private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db,
   1641             final ContentValues values, final String accountId) {
   1642         if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) ||
   1643                 values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
   1644             return restartPush(context, db, accountId);
   1645         }
   1646         return false;
   1647     }
   1648 
   1649     /**
   1650      * Restart push if an account's settings change in a way that requires it.
   1651      * @param context A {@link Context}.
   1652      * @param db The {@link SQLiteDatabase}.
   1653      * @param values The {@link ContentValues} that were updated for the account.
   1654      * @param accountId The id of the account.
   1655      * @return Whether or not the push was restarted.
   1656      */
   1657     private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db,
   1658             final ContentValues values, final String accountId) {
   1659         if (values.containsKey(AccountColumns.SYNC_LOOKBACK) ||
   1660                 values.containsKey(AccountColumns.SYNC_INTERVAL)) {
   1661             return restartPush(context, db, accountId);
   1662         }
   1663         return false;
   1664     }
   1665 
   1666     @Override
   1667     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   1668         LogUtils.d(TAG, "Update: " + uri);
   1669         // Handle this special case the fastest possible way
   1670         if (uri == INTEGRITY_CHECK_URI) {
   1671             checkDatabases();
   1672             return 0;
   1673         } else if (uri == ACCOUNT_BACKUP_URI) {
   1674             return backupAccounts(getContext(), getDatabase(getContext()));
   1675         }
   1676 
   1677         // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
   1678         Uri notificationUri = EmailContent.CONTENT_URI;
   1679 
   1680         int match = findMatch(uri, "update");
   1681         Context context = getContext();
   1682         ContentResolver resolver = context.getContentResolver();
   1683         // See the comment at delete(), above
   1684         SQLiteDatabase db = getDatabase(context);
   1685         int table = match >> BASE_SHIFT;
   1686         int result;
   1687 
   1688         // We do NOT allow setting of unreadCount/messageCount via the provider
   1689         // These columns are maintained via triggers
   1690         if (match == MAILBOX_ID || match == MAILBOX) {
   1691             values.remove(MailboxColumns.UNREAD_COUNT);
   1692             values.remove(MailboxColumns.MESSAGE_COUNT);
   1693         }
   1694 
   1695         String tableName = TABLE_NAMES.valueAt(table);
   1696         String id = "0";
   1697 
   1698         try {
   1699             switch (match) {
   1700                 case ACCOUNT_PICK_TRASH_FOLDER:
   1701                     return pickTrashFolder(uri);
   1702                 case ACCOUNT_PICK_SENT_FOLDER:
   1703                     return pickSentFolder(uri);
   1704                 case UI_FOLDER:
   1705                     return uiUpdateFolder(context, uri, values);
   1706                 case UI_RECENT_FOLDERS:
   1707                     return uiUpdateRecentFolders(uri, values);
   1708                 case UI_DEFAULT_RECENT_FOLDERS:
   1709                     return uiPopulateRecentFolders(uri);
   1710                 case UI_ATTACHMENT:
   1711                     return uiUpdateAttachment(uri, values);
   1712                 case UI_MESSAGE:
   1713                     return uiUpdateMessage(uri, values);
   1714                 case ACCOUNT_CHECK:
   1715                     id = uri.getLastPathSegment();
   1716                     // With any error, return 1 (a failure)
   1717                     int res = 1;
   1718                     Cursor ic = null;
   1719                     try {
   1720                         ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id});
   1721                         if (ic.moveToFirst()) {
   1722                             res = ic.getInt(0);
   1723                         }
   1724                     } finally {
   1725                         if (ic != null) {
   1726                             ic.close();
   1727                         }
   1728                     }
   1729                     // Count of duplicated mailboxes
   1730                     return res;
   1731                 case MESSAGE_SELECTION:
   1732                     Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
   1733                             selectionArgs, null, null, null);
   1734                     try {
   1735                         if (findCursor.moveToFirst()) {
   1736                             return update(ContentUris.withAppendedId(
   1737                                     Message.CONTENT_URI,
   1738                                     findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
   1739                                     values, null, null);
   1740                         } else {
   1741                             return 0;
   1742                         }
   1743                     } finally {
   1744                         findCursor.close();
   1745                     }
   1746                 case SYNCED_MESSAGE_ID:
   1747                 case UPDATED_MESSAGE_ID:
   1748                 case MESSAGE_ID:
   1749                 case BODY_ID:
   1750                 case ATTACHMENT_ID:
   1751                 case MAILBOX_ID:
   1752                 case ACCOUNT_ID:
   1753                 case HOSTAUTH_ID:
   1754                 case QUICK_RESPONSE_ID:
   1755                 case POLICY_ID:
   1756                     id = uri.getPathSegments().get(1);
   1757                     if (match == SYNCED_MESSAGE_ID) {
   1758                         // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well.
   1759                         boolean isEas = false;
   1760                         long mailboxId = -1;
   1761                         long accountId = -1;
   1762                         final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id});
   1763                         if (c != null) {
   1764                             try {
   1765                                 if (c.moveToFirst()) {
   1766                                     final String protocol = c.getString(INDEX_PROTOCOL);
   1767                                     isEas = context.getString(R.string.protocol_eas)
   1768                                             .equals(protocol);
   1769                                     mailboxId = c.getLong(INDEX_MAILBOX_KEY);
   1770                                     accountId = c.getLong(INDEX_ACCOUNT_KEY);
   1771                                 }
   1772                             } finally {
   1773                                 c.close();
   1774                             }
   1775                         }
   1776 
   1777                         if (isEas) {
   1778                             // EAS uses the new upsync classes.
   1779                             Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
   1780                             if (dstFolderId != null) {
   1781                                 addToMessageMove(db, id, dstFolderId);
   1782                             }
   1783                             Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
   1784                             Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
   1785                             int flagReadValue = (flagRead != null) ?
   1786                                     flagRead : MessageStateChange.VALUE_UNCHANGED;
   1787                             int flagFavoriteValue = (flagFavorite != null) ?
   1788                                     flagFavorite : MessageStateChange.VALUE_UNCHANGED;
   1789                             if (flagRead != null || flagFavorite != null) {
   1790                                 addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
   1791                             }
   1792 
   1793                             // Request a sync for the messages mailbox so the update will upsync.
   1794                             // This is normally done with ContentResolver.notifyUpdate() but doesn't
   1795                             // work for Exchange because the Sync Adapter is declared as
   1796                             // android:supportsUploading="false". Changing it to true is not trivial
   1797                             // because that would require us to protect all calls to notifyUpdate()
   1798                             // with syncToServer=false except in cases where we actually want to
   1799                             // upsync.
   1800                             // TODO: Look into making Exchange Sync Adapter supportsUploading=true
   1801                             // Since we can't use the Sync Manager "delayed-sync" feature which
   1802                             // applies only to UPLOAD syncs, we need to do this ourselves. The
   1803                             // purpose of this is not to spam syncs when making frequent
   1804                             // modifications.
   1805                             final Handler handler = getDelayedSyncHandler();
   1806                             final android.accounts.Account amAccount =
   1807                                     getAccountManagerAccount(accountId);
   1808                             if (amAccount != null) {
   1809                                 final SyncRequestMessage request = new SyncRequestMessage(
   1810                                         uri.getAuthority(), amAccount, mailboxId);
   1811                                 synchronized (mDelayedSyncRequests) {
   1812                                     if (!mDelayedSyncRequests.contains(request)) {
   1813                                         mDelayedSyncRequests.add(request);
   1814                                         final android.os.Message message =
   1815                                                 handler.obtainMessage(0, request);
   1816                                         handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS);
   1817                                     }
   1818                                 }
   1819                             } else {
   1820                                 LogUtils.d(TAG,
   1821                                         "Attempted to start delayed sync for invalid account %d",
   1822                                         accountId);
   1823                             }
   1824                         } else {
   1825                             // Old way of doing upsync.
   1826                             // For synced messages, first copy the old message to the updated table
   1827                             // Note the insert or ignore semantics, guaranteeing that only the first
   1828                             // update will be reflected in the updated message table; therefore this
   1829                             // row will always have the "original" data
   1830                             db.execSQL(UPDATED_MESSAGE_INSERT + id);
   1831                         }
   1832                     } else if (match == MESSAGE_ID) {
   1833                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
   1834                     }
   1835                     result = db.update(tableName, values, whereWithId(id, selection),
   1836                             selectionArgs);
   1837                     if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
   1838                         handleMessageUpdateNotifications(uri, id, values);
   1839                     } else if (match == ATTACHMENT_ID) {
   1840                         long attId = Integer.parseInt(id);
   1841                         if (values.containsKey(Attachment.FLAGS)) {
   1842                             int flags = values.getAsInteger(Attachment.FLAGS);
   1843                             mAttachmentService.attachmentChanged(context, attId, flags);
   1844                         }
   1845                         // Notify UI if necessary; there are only two columns we can change that
   1846                         // would be worth a notification
   1847                         if (values.containsKey(AttachmentColumns.UI_STATE) ||
   1848                                 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
   1849                             // Notify on individual attachment
   1850                             notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
   1851                             Attachment att = Attachment.restoreAttachmentWithId(context, attId);
   1852                             if (att != null) {
   1853                                 // And on owning Message
   1854                                 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
   1855                             }
   1856                         }
   1857                     } else if (match == MAILBOX_ID) {
   1858                         final long accountId = Mailbox.getAccountIdForMailbox(context, id);
   1859                         notifyUIFolder(id, accountId);
   1860                         restartPushForMailbox(context, db, values, Long.toString(accountId));
   1861                     } else if (match == ACCOUNT_ID) {
   1862                         updateAccountSyncInterval(Long.parseLong(id), values);
   1863                         // Notify individual account and "all accounts"
   1864                         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
   1865                         resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
   1866                         restartPushForAccount(context, db, values, id);
   1867                     }
   1868                     break;
   1869                 case BODY:
   1870                     result = db.update(tableName, values, selection, selectionArgs);
   1871                     if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) {
   1872                         // TODO: This is a hack. Notably, the selection equality test above
   1873                         // is hokey at best.
   1874                         LogUtils.i(TAG, "Body Update to non-existent row, morphing to insert");
   1875                         final ContentValues insertValues = new ContentValues(values);
   1876                         insertValues.put(EmailContent.Body.MESSAGE_KEY, selectionArgs[0]);
   1877                         insert(EmailContent.Body.CONTENT_URI, insertValues);
   1878                     }
   1879                     break;
   1880                 case MESSAGE:
   1881                 case UPDATED_MESSAGE:
   1882                 case ATTACHMENT:
   1883                 case MAILBOX:
   1884                 case ACCOUNT:
   1885                 case HOSTAUTH:
   1886                 case POLICY:
   1887                     if (match == ATTACHMENT) {
   1888                         if (values.containsKey(AttachmentColumns.LOCATION) &&
   1889                                 TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
   1890                             LogUtils.w(TAG, new Throwable(), "attachment with blank location");
   1891                         }
   1892                     }
   1893                     result = db.update(tableName, values, selection, selectionArgs);
   1894                     break;
   1895 
   1896                 case ACCOUNT_RESET_NEW_COUNT_ID:
   1897                     id = uri.getPathSegments().get(1);
   1898                     ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
   1899                     if (values != null) {
   1900                         Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME);
   1901                         if (set != null) {
   1902                             newMessageCount = new ContentValues();
   1903                             newMessageCount.put(Account.NEW_MESSAGE_COUNT, set);
   1904                         }
   1905                     }
   1906                     result = db.update(tableName, newMessageCount,
   1907                             whereWithId(id, selection), selectionArgs);
   1908                     notificationUri = Account.CONTENT_URI; // Only notify account cursors.
   1909                     break;
   1910                 case ACCOUNT_RESET_NEW_COUNT:
   1911                     result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
   1912                             selection, selectionArgs);
   1913                     // Affects all accounts.  Just invalidate all account cache.
   1914                     notificationUri = Account.CONTENT_URI; // Only notify account cursors.
   1915                     break;
   1916                 case MESSAGE_MOVE:
   1917                     result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs);
   1918                     break;
   1919                 case MESSAGE_STATE_CHANGE:
   1920                     result = db.update(MessageStateChange.TABLE_NAME, values, selection,
   1921                             selectionArgs);
   1922                     break;
   1923                 default:
   1924                     throw new IllegalArgumentException("Unknown URI " + uri);
   1925             }
   1926         } catch (SQLiteException e) {
   1927             checkDatabases();
   1928             throw e;
   1929         }
   1930 
   1931         // Notify all notifier cursors
   1932         sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
   1933 
   1934         resolver.notifyChange(notificationUri, null);
   1935         return result;
   1936     }
   1937 
   1938     private void updateSyncStatus(final Bundle extras) {
   1939         final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID);
   1940         final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE);
   1941         final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id);
   1942         EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null);
   1943         final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS;
   1944         if (inProgress) {
   1945             RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id);
   1946         } else {
   1947             final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT);
   1948             final ContentValues values = new ContentValues();
   1949             values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
   1950             mDatabase.update(
   1951                     Mailbox.TABLE_NAME,
   1952                     values,
   1953                     WHERE_ID,
   1954                     new String[] { String.valueOf(id) });
   1955         }
   1956     }
   1957 
   1958     @Override
   1959     public Bundle call(String method, String arg, Bundle extras) {
   1960         LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg);
   1961 
   1962         // Handle queries for the device friendly name.
   1963         // TODO: This should eventually be a device property, not defined by the app.
   1964         if (TextUtils.equals(method, EmailContent.DEVICE_FRIENDLY_NAME)) {
   1965             final Bundle bundle = new Bundle(1);
   1966             // TODO: For now, just use the model name since we don't yet have a user-supplied name.
   1967             bundle.putString(EmailContent.DEVICE_FRIENDLY_NAME, Build.MODEL);
   1968             return bundle;
   1969         }
   1970 
   1971         // Handle sync status callbacks.
   1972         if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) {
   1973             updateSyncStatus(extras);
   1974             return null;
   1975         }
   1976         if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) {
   1977             fixParentKeys(getDatabase(getContext()));
   1978             return null;
   1979         }
   1980 
   1981         // Handle send & save.
   1982         final Uri accountUri = Uri.parse(arg);
   1983         final long accountId = Long.parseLong(accountUri.getPathSegments().get(1));
   1984 
   1985         Uri messageUri = null;
   1986 
   1987         if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) {
   1988             messageUri = uiSendDraftMessage(accountId, extras);
   1989             Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId);
   1990         } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) {
   1991             messageUri = uiSaveDraftMessage(accountId, extras);
   1992         } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) {
   1993             LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method);
   1994         } else {
   1995             LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method);
   1996         }
   1997 
   1998         final Bundle result;
   1999         if (messageUri != null) {
   2000             result = new Bundle(1);
   2001             result.putParcelable(UIProvider.MessageColumns.URI, messageUri);
   2002         } else {
   2003             result = null;
   2004         }
   2005 
   2006         return result;
   2007     }
   2008 
   2009     @Override
   2010     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
   2011         if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
   2012             LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
   2013         }
   2014 
   2015         final int match = findMatch(uri, "openFile");
   2016         switch (match) {
   2017             case ATTACHMENTS_CACHED_FILE_ACCESS:
   2018                 // Parse the cache file path out from the uri
   2019                 final String cachedFilePath =
   2020                         uri.getQueryParameter(EmailContent.Attachment.CACHED_FILE_QUERY_PARAM);
   2021 
   2022                 if (cachedFilePath != null) {
   2023                     // clearCallingIdentity means that the download manager will
   2024                     // check our permissions rather than the permissions of whatever
   2025                     // code is calling us.
   2026                     long binderToken = Binder.clearCallingIdentity();
   2027                     try {
   2028                         LogUtils.d(TAG, "Opening attachment %s", cachedFilePath);
   2029                         return ParcelFileDescriptor.open(
   2030                                 new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
   2031                     } finally {
   2032                         Binder.restoreCallingIdentity(binderToken);
   2033                     }
   2034                 }
   2035                 break;
   2036         }
   2037 
   2038         throw new FileNotFoundException("unable to open file");
   2039     }
   2040 
   2041 
   2042     /**
   2043      * Returns the base notification URI for the given content type.
   2044      *
   2045      * @param match The type of content that was modified.
   2046      */
   2047     private static Uri getBaseNotificationUri(int match) {
   2048         Uri baseUri = null;
   2049         switch (match) {
   2050             case MESSAGE:
   2051             case MESSAGE_ID:
   2052             case SYNCED_MESSAGE_ID:
   2053                 baseUri = Message.NOTIFIER_URI;
   2054                 break;
   2055             case ACCOUNT:
   2056             case ACCOUNT_ID:
   2057                 baseUri = Account.NOTIFIER_URI;
   2058                 break;
   2059         }
   2060         return baseUri;
   2061     }
   2062 
   2063     /**
   2064      * Sends a change notification to any cursors observers of the given base URI. The final
   2065      * notification URI is dynamically built to contain the specified information. It will be
   2066      * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
   2067      * upon the given values.
   2068      * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
   2069      * If this is necessary, it can be added. However, due to the implementation of
   2070      * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
   2071      *
   2072      * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
   2073      * @param op Optional operation to be appended to the URI.
   2074      * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
   2075      *           appended to the base URI.
   2076      */
   2077     private void sendNotifierChange(Uri baseUri, String op, String id) {
   2078         if (baseUri == null) return;
   2079 
   2080         final ContentResolver resolver = getContext().getContentResolver();
   2081 
   2082         // Append the operation, if specified
   2083         if (op != null) {
   2084             baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
   2085         }
   2086 
   2087         long longId = 0L;
   2088         try {
   2089             longId = Long.valueOf(id);
   2090         } catch (NumberFormatException ignore) {}
   2091         if (longId > 0) {
   2092             resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null);
   2093         } else {
   2094             resolver.notifyChange(baseUri, null);
   2095         }
   2096 
   2097         // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
   2098         if (baseUri.equals(Message.NOTIFIER_URI)) {
   2099             sendMessageListDataChangedNotification();
   2100         }
   2101     }
   2102 
   2103     private void sendMessageListDataChangedNotification() {
   2104         final Context context = getContext();
   2105         final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
   2106         // Ideally this intent would contain information about which account changed, to limit the
   2107         // updates to that particular account.  Unfortunately, that information is not available in
   2108         // sendNotifierChange().
   2109         context.sendBroadcast(intent);
   2110     }
   2111 
   2112     @Override
   2113     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   2114             throws OperationApplicationException {
   2115         Context context = getContext();
   2116         SQLiteDatabase db = getDatabase(context);
   2117         db.beginTransaction();
   2118         try {
   2119             ContentProviderResult[] results = super.applyBatch(operations);
   2120             db.setTransactionSuccessful();
   2121             return results;
   2122         } finally {
   2123             db.endTransaction();
   2124         }
   2125     }
   2126 
   2127     public static interface AttachmentService {
   2128         /**
   2129          * Notify the service that an attachment has changed.
   2130          */
   2131         void attachmentChanged(Context context, long id, int flags);
   2132     }
   2133 
   2134     private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() {
   2135         @Override
   2136         public void attachmentChanged(Context context, long id, int flags) {
   2137             // The default implementation delegates to the real service.
   2138             AttachmentDownloadService.attachmentChanged(context, id, flags);
   2139         }
   2140     };
   2141     private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
   2142 
   2143     // exposed for testing
   2144     public void injectAttachmentService(AttachmentService attachmentService) {
   2145         mAttachmentService =
   2146             attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService;
   2147     }
   2148 
   2149     private Cursor notificationQuery(final Uri uri) {
   2150         final SQLiteDatabase db = getDatabase(getContext());
   2151         final String accountId = uri.getLastPathSegment();
   2152 
   2153         final StringBuilder sqlBuilder = new StringBuilder();
   2154         sqlBuilder.append("SELECT ");
   2155         sqlBuilder.append(MessageColumns.MAILBOX_KEY).append(", ");
   2156         sqlBuilder.append("SUM(CASE ")
   2157                 .append(MessageColumns.FLAG_READ).append(" WHEN 0 THEN 1 ELSE 0 END), ");
   2158         sqlBuilder.append("SUM(CASE ")
   2159                 .append(MessageColumns.FLAG_SEEN).append(" WHEN 0 THEN 1 ELSE 0 END)\n");
   2160         sqlBuilder.append("FROM ");
   2161         sqlBuilder.append(Message.TABLE_NAME).append('\n');
   2162         sqlBuilder.append("WHERE ");
   2163         sqlBuilder.append(MessageColumns.ACCOUNT_KEY).append(" = ?\n");
   2164         sqlBuilder.append("GROUP BY ");
   2165         sqlBuilder.append(MessageColumns.MAILBOX_KEY);
   2166 
   2167         final String sql = sqlBuilder.toString();
   2168 
   2169         final String[] selectionArgs = {accountId};
   2170 
   2171         return db.rawQuery(sql, selectionArgs);
   2172     }
   2173 
   2174     public Cursor mostRecentMessageQuery(Uri uri) {
   2175         SQLiteDatabase db = getDatabase(getContext());
   2176         String mailboxId = uri.getLastPathSegment();
   2177         return db.rawQuery("select max(_id) from Message where mailboxKey=?",
   2178                 new String[] {mailboxId});
   2179     }
   2180 
   2181     private Cursor getMailboxMessageCount(Uri uri) {
   2182         SQLiteDatabase db = getDatabase(getContext());
   2183         String mailboxId = uri.getLastPathSegment();
   2184         return db.rawQuery("select count(*) from Message where mailboxKey=?",
   2185                 new String[] {mailboxId});
   2186     }
   2187 
   2188     /**
   2189      * Support for UnifiedEmail below
   2190      */
   2191 
   2192     private static final String NOT_A_DRAFT_STRING =
   2193         Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
   2194 
   2195     private static final String CONVERSATION_FLAGS =
   2196             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
   2197                 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE +
   2198                 " ELSE 0 END + " +
   2199             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED +
   2200                 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED +
   2201                 " ELSE 0 END + " +
   2202              "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO +
   2203                 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED +
   2204                 " ELSE 0 END";
   2205 
   2206     /**
   2207      * Array of pre-defined account colors (legacy colors from old email app)
   2208      */
   2209     private static final int[] ACCOUNT_COLORS = new int[] {
   2210         0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79,
   2211         0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4
   2212     };
   2213 
   2214     private static final String CONVERSATION_COLOR =
   2215             "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length +
   2216                     " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
   2217                     " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
   2218                     " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
   2219                     " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
   2220                     " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
   2221                     " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
   2222                     " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
   2223                     " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
   2224                     " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
   2225             " END";
   2226 
   2227     private static final String ACCOUNT_COLOR =
   2228             "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length +
   2229                     " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
   2230                     " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
   2231                     " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
   2232                     " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
   2233                     " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
   2234                     " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
   2235                     " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
   2236                     " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
   2237                     " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
   2238             " END";
   2239     /**
   2240      * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
   2241      * conversation list in UnifiedEmail)
   2242      */
   2243     private static ProjectionMap getMessageListMap() {
   2244         if (sMessageListMap == null) {
   2245             sMessageListMap = ProjectionMap.builder()
   2246                 .add(BaseColumns._ID, MessageColumns.ID)
   2247                 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
   2248                 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
   2249                 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
   2250                 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
   2251                 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null)
   2252                 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
   2253                 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
   2254                 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
   2255                 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
   2256                 .add(UIProvider.ConversationColumns.SENDING_STATE,
   2257                         Integer.toString(ConversationSendingState.OTHER))
   2258                 .add(UIProvider.ConversationColumns.PRIORITY,
   2259                         Integer.toString(ConversationPriority.LOW))
   2260                 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
   2261                 .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN)
   2262                 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
   2263                 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS)
   2264                 .add(UIProvider.ConversationColumns.ACCOUNT_URI,
   2265                         uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
   2266                 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
   2267                 .build();
   2268         }
   2269         return sMessageListMap;
   2270     }
   2271     private static ProjectionMap sMessageListMap;
   2272 
   2273     /**
   2274      * Generate UIProvider draft type; note the test for "reply all" must come before "reply"
   2275      */
   2276     private static final String MESSAGE_DRAFT_TYPE =
   2277         "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL +
   2278             ") !=0 THEN " + UIProvider.DraftType.COMPOSE +
   2279         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL +
   2280             ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL +
   2281         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY +
   2282             ") !=0 THEN " + UIProvider.DraftType.REPLY +
   2283         " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD +
   2284             ") !=0 THEN " + UIProvider.DraftType.FORWARD +
   2285             " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END";
   2286 
   2287     private static final String MESSAGE_FLAGS =
   2288             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
   2289             ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE +
   2290             " ELSE 0 END";
   2291 
   2292     /**
   2293      * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
   2294      * UnifiedEmail
   2295      */
   2296     private static ProjectionMap getMessageViewMap() {
   2297         if (sMessageViewMap == null) {
   2298             sMessageViewMap = ProjectionMap.builder()
   2299                 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID)
   2300                 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
   2301                 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
   2302                 .add(UIProvider.MessageColumns.CONVERSATION_ID,
   2303                         uriWithFQId("uimessage", Message.TABLE_NAME))
   2304                 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT)
   2305                 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET)
   2306                 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST)
   2307                 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST)
   2308                 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST)
   2309                 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST)
   2310                 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST)
   2311                 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS,
   2312                         EmailContent.MessageColumns.TIMESTAMP)
   2313                 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT)
   2314                 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT)
   2315                 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
   2316                 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
   2317                 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
   2318                 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS,
   2319                         EmailContent.MessageColumns.FLAG_ATTACHMENT)
   2320                 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
   2321                         uriWithFQId("uiattachments", Message.TABLE_NAME))
   2322                 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS)
   2323                 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE)
   2324                 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI,
   2325                         uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
   2326                 .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE)
   2327                 .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ)
   2328                 .add(UIProvider.MessageColumns.SEEN, EmailContent.MessageColumns.FLAG_SEEN)
   2329                 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null)
   2330                 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL,
   2331                         Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING))
   2332                 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE,
   2333                         Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK))
   2334                 .add(UIProvider.MessageColumns.VIA_DOMAIN, null)
   2335                 .build();
   2336         }
   2337         return sMessageViewMap;
   2338     }
   2339     private static ProjectionMap sMessageViewMap;
   2340 
   2341     /**
   2342      * Generate UIProvider folder capabilities from mailbox flags
   2343      */
   2344     private static final String FOLDER_CAPABILITIES =
   2345         "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
   2346             ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
   2347             " ELSE 0 END";
   2348 
   2349     /**
   2350      * Convert EmailProvider type to UIProvider type
   2351      */
   2352     private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE
   2353             + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + UIProvider.FolderType.INBOX
   2354             + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + UIProvider.FolderType.DRAFT
   2355             + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + UIProvider.FolderType.OUTBOX
   2356             + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + UIProvider.FolderType.SENT
   2357             + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + UIProvider.FolderType.TRASH
   2358             + " WHEN " + Mailbox.TYPE_JUNK    + " THEN " + UIProvider.FolderType.SPAM
   2359             + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED
   2360             + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD
   2361             + " WHEN " + Mailbox.TYPE_SEARCH + " THEN "
   2362                     + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH)
   2363             + " ELSE " + UIProvider.FolderType.DEFAULT + " END";
   2364 
   2365     private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE
   2366             + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + R.drawable.ic_folder_inbox
   2367             + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + R.drawable.ic_folder_drafts
   2368             + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + R.drawable.ic_folder_outbox
   2369             + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + R.drawable.ic_folder_sent
   2370             + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + R.drawable.ic_folder_trash
   2371             + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_folder_star
   2372             + " ELSE -1 END";
   2373 
   2374     /**
   2375      * Local-only folders set totalCount < 0; such folders should substitute message count for
   2376      * total count.
   2377      * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types.
   2378      */
   2379     private static final String TOTAL_COUNT = "CASE WHEN "
   2380             + MailboxColumns.TOTAL_COUNT + "<0 OR "
   2381             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR "
   2382             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR "
   2383             + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH
   2384             + " THEN " + MailboxColumns.MESSAGE_COUNT
   2385             + " ELSE " + MailboxColumns.TOTAL_COUNT + " END";
   2386 
   2387     private static ProjectionMap getFolderListMap() {
   2388         if (sFolderListMap == null) {
   2389             sFolderListMap = ProjectionMap.builder()
   2390                 .add(BaseColumns._ID, MailboxColumns.ID)
   2391                 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID)
   2392                 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
   2393                 .add(UIProvider.FolderColumns.NAME, "displayName")
   2394                 .add(UIProvider.FolderColumns.HAS_CHILDREN,
   2395                         MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
   2396                 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES)
   2397                 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
   2398                 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
   2399                 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
   2400                 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
   2401                 .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT)
   2402                 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH))
   2403                 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
   2404                 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
   2405                 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE)
   2406                 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON)
   2407                 .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore"))
   2408                 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME)
   2409                 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY
   2410                         + "=" + Mailbox.NO_MAILBOX + " then NULL else " +
   2411                         uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end")
   2412                 /**
   2413                  * SELECT group_concat(fromList) FROM
   2414                  * (SELECT fromList FROM message WHERE mailboxKey=? AND flagRead=0
   2415                  *  GROUP BY fromList ORDER BY timestamp DESC)
   2416                  */
   2417                 .add(UIProvider.FolderColumns.UNREAD_SENDERS,
   2418                         "(SELECT group_concat(" + MessageColumns.FROM_LIST + ") FROM " +
   2419                         "(SELECT " + MessageColumns.FROM_LIST + " FROM " + Message.TABLE_NAME +
   2420                         " WHERE " + MessageColumns.MAILBOX_KEY + "=" + Mailbox.TABLE_NAME + "." +
   2421                         MailboxColumns.ID + " AND " + MessageColumns.FLAG_READ + "=0" +
   2422                         " GROUP BY " + MessageColumns.FROM_LIST + " ORDER BY " +
   2423                         MessageColumns.TIMESTAMP + " DESC))")
   2424                 .build();
   2425         }
   2426         return sFolderListMap;
   2427     }
   2428     private static ProjectionMap sFolderListMap;
   2429 
   2430     /**
   2431      * Constructs the map of default entries for accounts. These values can be overridden in
   2432      * {@link #genQueryAccount(String[], String)}.
   2433      */
   2434     private static ProjectionMap getAccountListMap(Context context) {
   2435         if (sAccountListMap == null) {
   2436             final MailPrefs mailPrefs = MailPrefs.get(context);
   2437 
   2438             final ProjectionMap.Builder builder = ProjectionMap.builder()
   2439                     .add(BaseColumns._ID, AccountColumns.ID)
   2440                     .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
   2441                     .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders"))
   2442                     .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders"))
   2443                     .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
   2444                     .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME,
   2445                             AccountColumns.EMAIL_ADDRESS)
   2446                     .add(UIProvider.AccountColumns.SENDER_NAME,
   2447                             AccountColumns.SENDER_NAME)
   2448                     .add(UIProvider.AccountColumns.UNDO_URI,
   2449                             ("'content://" + EmailContent.AUTHORITY + "/uiundo'"))
   2450                     .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
   2451                     .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
   2452                             // TODO: Is provider version used?
   2453                     .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
   2454                     .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
   2455                     .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI,
   2456                             uriWithId("uirecentfolders"))
   2457                     .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
   2458                             uriWithId("uidefaultrecentfolders"))
   2459                     .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE,
   2460                             AccountColumns.SIGNATURE)
   2461                     .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS,
   2462                             Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
   2463                     .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0")
   2464                     .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
   2465                             Integer.toString(UIProvider.ConversationViewMode.UNDEFINED))
   2466                     .add(UIProvider.AccountColumns.SettingsColumns.MAX_ATTACHMENT_SIZE,
   2467                             AccountColumns.MAX_ATTACHMENT_SIZE)
   2468                     .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null);
   2469 
   2470             final String feedbackUri = context.getString(R.string.email_feedback_uri);
   2471             if (!TextUtils.isEmpty(feedbackUri)) {
   2472                 // This string needs to be in single quotes, as it will be used as a constant
   2473                 // in a sql expression
   2474                 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI,
   2475                         "'" + feedbackUri + "'");
   2476             }
   2477 
   2478             sAccountListMap = builder.build();
   2479         }
   2480         return sAccountListMap;
   2481     }
   2482     private static ProjectionMap sAccountListMap;
   2483 
   2484     private static ProjectionMap getQuickResponseMap() {
   2485         if (sQuickResponseMap == null) {
   2486             sQuickResponseMap = ProjectionMap.builder()
   2487                     .add(UIProvider.QuickResponseColumns.TEXT,
   2488                             EmailContent.QuickResponseColumns.TEXT)
   2489                     .add(UIProvider.QuickResponseColumns.URI,
   2490                             "'" + combinedUriString("quickresponse", "") + "'||"
   2491                                     + EmailContent.QuickResponseColumns.ID)
   2492                     .build();
   2493         }
   2494         return sQuickResponseMap;
   2495     }
   2496     private static ProjectionMap sQuickResponseMap;
   2497 
   2498     /**
   2499      * The "ORDER BY" clause for top level folders
   2500      */
   2501     private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
   2502         + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
   2503         + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
   2504         + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
   2505         + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
   2506         + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
   2507         + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
   2508         // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
   2509         + " ELSE 10 END"
   2510         + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
   2511 
   2512     /**
   2513      * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
   2514      */
   2515     private static ProjectionMap getAttachmentMap() {
   2516         if (sAttachmentMap == null) {
   2517             sAttachmentMap = ProjectionMap.builder()
   2518                 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
   2519                 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
   2520                 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
   2521                 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
   2522                 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
   2523                 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
   2524                 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE,
   2525                         AttachmentColumns.UI_DOWNLOADED_SIZE)
   2526                 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
   2527                 .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS)
   2528                 .build();
   2529         }
   2530         return sAttachmentMap;
   2531     }
   2532     private static ProjectionMap sAttachmentMap;
   2533 
   2534     /**
   2535      * Generate the SELECT clause using a specified mapping and the original UI projection
   2536      * @param map the ProjectionMap to use for this projection
   2537      * @param projection the projection as sent by UnifiedEmail
   2538      * @return a StringBuilder containing the SELECT expression for a SQLite query
   2539      */
   2540     private static StringBuilder genSelect(ProjectionMap map, String[] projection) {
   2541         return genSelect(map, projection, EMPTY_CONTENT_VALUES);
   2542     }
   2543 
   2544     private static StringBuilder genSelect(ProjectionMap map, String[] projection,
   2545             ContentValues values) {
   2546         final StringBuilder sb = new StringBuilder("SELECT ");
   2547         boolean first = true;
   2548         for (final String column: projection) {
   2549             if (first) {
   2550                 first = false;
   2551             } else {
   2552                 sb.append(',');
   2553             }
   2554             final String val;
   2555             // First look at values; this is an override of default behavior
   2556             if (values.containsKey(column)) {
   2557                 final String value = values.getAsString(column);
   2558                 if (value == null) {
   2559                     val = "NULL AS " + column;
   2560                 } else if (value.startsWith("@")) {
   2561                     val = value.substring(1) + " AS " + column;
   2562                 } else {
   2563                     val = "'" + value + "' AS " + column;
   2564                 }
   2565             } else {
   2566                 // Now, get the standard value for the column from our projection map
   2567                 final String mapVal = map.get(column);
   2568                 // If we don't have the column, return "NULL AS <column>", and warn
   2569                 if (mapVal == null) {
   2570                     val = "NULL AS " + column;
   2571                     // Apparently there's a lot of these, so don't spam the log with warnings
   2572                     // LogUtils.w(TAG, "column " + column + " missing from projection map");
   2573                 } else {
   2574                     val = mapVal;
   2575                 }
   2576             }
   2577             sb.append(val);
   2578         }
   2579         return sb;
   2580     }
   2581 
   2582     /**
   2583      * Convenience method to create a Uri string given the "type" of query; we append the type
   2584      * of the query and the id column name (_id)
   2585      *
   2586      * @param type the "type" of the query, as defined by our UriMatcher definitions
   2587      * @return a Uri string
   2588      */
   2589     private static String uriWithId(String type) {
   2590         return uriWithColumn(type, EmailContent.RECORD_ID);
   2591     }
   2592 
   2593     /**
   2594      * Convenience method to create a Uri string given the "type" of query; we append the type
   2595      * of the query and the passed in column name
   2596      *
   2597      * @param type the "type" of the query, as defined by our UriMatcher definitions
   2598      * @param columnName the column in the table being queried
   2599      * @return a Uri string
   2600      */
   2601     private static String uriWithColumn(String type, String columnName) {
   2602         return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName;
   2603     }
   2604 
   2605     /**
   2606      * Convenience method to create a Uri string given the "type" of query and the table name to
   2607      * which it applies; we append the type of the query and the fully qualified (FQ) id column
   2608      * (i.e. including the table name); we need this for join queries where _id would otherwise
   2609      * be ambiguous
   2610      *
   2611      * @param type the "type" of the query, as defined by our UriMatcher definitions
   2612      * @param tableName the name of the table whose _id is referred to
   2613      * @return a Uri string
   2614      */
   2615     private static String uriWithFQId(String type, String tableName) {
   2616         return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
   2617     }
   2618 
   2619     // Regex that matches start of img tag. '<(?i)img\s+'.
   2620     private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
   2621 
   2622     /**
   2623      * Class that holds the sqlite query and the attachment (JSON) value (which might be null)
   2624      */
   2625     private static class MessageQuery {
   2626         final String query;
   2627         final String attachmentJson;
   2628 
   2629         MessageQuery(String _query, String _attachmentJson) {
   2630             query = _query;
   2631             attachmentJson = _attachmentJson;
   2632         }
   2633     }
   2634 
   2635     /**
   2636      * Generate the "view message" SQLite query, given a projection from UnifiedEmail
   2637      *
   2638      * @param uiProjection as passed from UnifiedEmail
   2639      * @return the SQLite query to be executed on the EmailProvider database
   2640      */
   2641     private MessageQuery genQueryViewMessage(String[] uiProjection, String id) {
   2642         Context context = getContext();
   2643         long messageId = Long.parseLong(id);
   2644         Message msg = Message.restoreMessageWithId(context, messageId);
   2645         ContentValues values = new ContentValues();
   2646         String attachmentJson = null;
   2647         if (msg != null) {
   2648             Body body = Body.restoreBodyWithMessageId(context, messageId);
   2649             if (body != null) {
   2650                 if (body.mHtmlContent != null) {
   2651                     if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) {
   2652                         values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1);
   2653                     }
   2654                 }
   2655             }
   2656             Address[] fromList = Address.unpack(msg.mFrom);
   2657             int autoShowImages = 0;
   2658             final MailPrefs mailPrefs = MailPrefs.get(context);
   2659             for (Address sender : fromList) {
   2660                 final String email = sender.getAddress();
   2661                 if (mailPrefs.getDisplayImagesFromSender(email)) {
   2662                     autoShowImages = 1;
   2663                     break;
   2664                 }
   2665             }
   2666             values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages);
   2667             // Add attachments...
   2668             Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
   2669             if (atts.length > 0) {
   2670                 ArrayList<com.android.mail.providers.Attachment> uiAtts =
   2671                         new ArrayList<com.android.mail.providers.Attachment>();
   2672                 for (Attachment att : atts) {
   2673                     // TODO: This code is intended to strip out any inlined attachments (which
   2674                     // would have a non-null contentId) so that they will not display at the bottom
   2675                     // along with the non-inlined attachments.
   2676                     // The problem is that the UI_ATTACHMENTS query does not behave the same way,
   2677                     // which causes crazy formatting.
   2678                     // There is an open question here, should attachments that are inlined
   2679                     // ALSO appear in the list of attachments at the bottom with the non-inlined
   2680                     // attachments?
   2681                     // Either way, the two queries need to behave the same way.
   2682                     // As of now, they will. If we decide to stop this, then we need to enable
   2683                     // the code below, and then also make the UI_ATTACHMENTS query behave
   2684                     // the same way.
   2685 //
   2686 //                    if (att.mContentId != null && att.getContentUri() != null) {
   2687 //                        continue;
   2688 //                    }
   2689                     com.android.mail.providers.Attachment uiAtt =
   2690                             new com.android.mail.providers.Attachment();
   2691                     uiAtt.setName(att.mFileName);
   2692                     uiAtt.setContentType(att.mMimeType);
   2693                     uiAtt.size = (int) att.mSize;
   2694                     uiAtt.uri = uiUri("uiattachment", att.mId);
   2695                     uiAtt.flags = att.mFlags;
   2696                     uiAtts.add(uiAtt);
   2697                 }
   2698                 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal
   2699                 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts);
   2700             }
   2701             if (msg.mDraftInfo != 0) {
   2702                 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT,
   2703                         (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0);
   2704                 values.put(UIProvider.MessageColumns.QUOTE_START_POS,
   2705                         msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK);
   2706             }
   2707             if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
   2708                 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI,
   2709                         "content://ui.email2.android.com/event/" + msg.mId);
   2710             }
   2711             /**
   2712              * HACK: override the attachment uri to contain a query parameter
   2713              * This forces the message footer to reload the attachment display when the message is
   2714              * fully loaded.
   2715              */
   2716             final Uri attachmentListUri = uiUri("uiattachments", messageId).buildUpon()
   2717                     .appendQueryParameter("MessageLoaded",
   2718                             msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE ? "true" : "false")
   2719                     .build();
   2720             values.put(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, attachmentListUri.toString());
   2721         }
   2722         StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values);
   2723         sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + " ON " +
   2724                 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " WHERE " +
   2725                 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?");
   2726         String sql = sb.toString();
   2727         return new MessageQuery(sql, attachmentJson);
   2728     }
   2729 
   2730     private static void appendConversationInfoColumns(final StringBuilder stringBuilder) {
   2731         // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :(
   2732         // There may be a better way to do this, but since the projection is specified by the
   2733         // unified UI code, it can't ask for these columns.
   2734         stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME)
   2735                 .append(',').append(MessageColumns.FROM_LIST);
   2736     }
   2737 
   2738     /**
   2739      * Generate the "message list" SQLite query, given a projection from UnifiedEmail
   2740      *
   2741      * @param uiProjection as passed from UnifiedEmail
   2742      * @param unseenOnly <code>true</code> to only return unseen messages
   2743      * @return the SQLite query to be executed on the EmailProvider database
   2744      */
   2745     private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) {
   2746         StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
   2747         appendConversationInfoColumns(sb);
   2748         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
   2749                 Message.FLAG_LOADED_SELECTION + " AND " +
   2750                 Message.MAILBOX_KEY + "=? ");
   2751         if (unseenOnly) {
   2752             sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 ");
   2753             sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 ");
   2754         }
   2755         sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC ");
   2756         sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT);
   2757         return sb.toString();
   2758     }
   2759 
   2760     /**
   2761      * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail
   2762      *
   2763      * @param uiProjection as passed from UnifiedEmail
   2764      * @param mailboxId the id of the virtual mailbox
   2765      * @param unseenOnly <code>true</code> to only return unseen messages
   2766      * @return the SQLite query to be executed on the EmailProvider database
   2767      */
   2768     private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection,
   2769             long mailboxId, final boolean unseenOnly) {
   2770         ContentValues values = new ContentValues();
   2771         values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR);
   2772         final int virtualMailboxId = getVirtualMailboxType(mailboxId);
   2773         final String[] selectionArgs;
   2774         StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values);
   2775         appendConversationInfoColumns(sb);
   2776         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
   2777                 Message.FLAG_LOADED_SELECTION + " AND ");
   2778         if (isCombinedMailbox(mailboxId)) {
   2779             if (unseenOnly) {
   2780                 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND ");
   2781                 sb.append(MessageColumns.FLAG_READ).append("=0 AND ");
   2782             }
   2783             selectionArgs = null;
   2784         } else {
   2785             if (virtualMailboxId == Mailbox.TYPE_INBOX) {
   2786                 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
   2787             }
   2788             sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND ");
   2789             selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)};
   2790         }
   2791         switch (getVirtualMailboxType(mailboxId)) {
   2792             case Mailbox.TYPE_INBOX:
   2793                 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID +
   2794                         " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE +
   2795                         "=" + Mailbox.TYPE_INBOX + ")");
   2796                 break;
   2797             case Mailbox.TYPE_STARRED:
   2798                 sb.append(MessageColumns.FLAG_FAVORITE + "=1");
   2799                 break;
   2800             case Mailbox.TYPE_UNREAD:
   2801                 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY +
   2802                         " NOT IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME +
   2803                         " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")");
   2804                 break;
   2805             default:
   2806                 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
   2807         }
   2808         sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC");
   2809         return db.rawQuery(sb.toString(), selectionArgs);
   2810     }
   2811 
   2812     /**
   2813      * Generate the "message list" SQLite query, given a projection from UnifiedEmail
   2814      *
   2815      * @param uiProjection as passed from UnifiedEmail
   2816      * @return the SQLite query to be executed on the EmailProvider database
   2817      */
   2818     private static String genQueryConversation(String[] uiProjection) {
   2819         StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
   2820         sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?");
   2821         return sb.toString();
   2822     }
   2823 
   2824     /**
   2825      * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
   2826      *
   2827      * @param uiProjection as passed from UnifiedEmail
   2828      * @return the SQLite query to be executed on the EmailProvider database
   2829      */
   2830     private static String genQueryAccountMailboxes(String[] uiProjection) {
   2831         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   2832         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   2833                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   2834                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   2835                 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
   2836         sb.append(MAILBOX_ORDER_BY);
   2837         return sb.toString();
   2838     }
   2839 
   2840     /**
   2841      * Generate the "all folders" SQLite query, given a projection from UnifiedEmail.  The list is
   2842      * sorted by the name as it appears in a hierarchical listing
   2843      *
   2844      * @param uiProjection as passed from UnifiedEmail
   2845      * @return the SQLite query to be executed on the EmailProvider database
   2846      */
   2847     private static String genQueryAccountAllMailboxes(String[] uiProjection) {
   2848         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   2849         // Use a derived column to choose either hierarchicalName or displayName
   2850         sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " +
   2851                 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME +
   2852                 " end as h_name");
   2853         // Order by the derived column
   2854         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   2855                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   2856                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   2857                 " ORDER BY h_name");
   2858         return sb.toString();
   2859     }
   2860 
   2861     /**
   2862      * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail
   2863      *
   2864      * @param uiProjection as passed from UnifiedEmail
   2865      * @return the SQLite query to be executed on the EmailProvider database
   2866      */
   2867     private static String genQueryRecentMailboxes(String[] uiProjection) {
   2868         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   2869         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
   2870                 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
   2871                 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
   2872                 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " +
   2873                 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " +
   2874                 MailboxColumns.LAST_TOUCHED_TIME + " DESC");
   2875         return sb.toString();
   2876     }
   2877 
   2878     private int getFolderCapabilities(EmailServiceInfo info, int type, long mailboxId) {
   2879         // Special case for Search folders: only permit delete, do not try to give any other caps.
   2880         if (type == Mailbox.TYPE_SEARCH) {
   2881             return UIProvider.FolderCapabilities.DELETE;
   2882         }
   2883 
   2884         // All folders support delete, except drafts.
   2885         int caps = 0;
   2886         if (type != Mailbox.TYPE_DRAFTS) {
   2887             caps = UIProvider.FolderCapabilities.DELETE;
   2888         }
   2889         if (info != null && info.offerLookback) {
   2890             // Protocols supporting lookback support settings
   2891             caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS;
   2892         }
   2893 
   2894         if (type == Mailbox.TYPE_MAIL || type == Mailbox.TYPE_TRASH ||
   2895                 type == Mailbox.TYPE_JUNK || type == Mailbox.TYPE_INBOX) {
   2896             // If the mailbox can accept moved mail, report that as well
   2897             caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES;
   2898             caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION;
   2899         }
   2900 
   2901         // For trash, we don't allow undo
   2902         if (type == Mailbox.TYPE_TRASH) {
   2903             caps =  UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES |
   2904                     UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION |
   2905                     UIProvider.FolderCapabilities.DELETE |
   2906                     UIProvider.FolderCapabilities.DELETE_ACTION_FINAL;
   2907         }
   2908         if (isVirtualMailbox(mailboxId)) {
   2909             caps |= UIProvider.FolderCapabilities.IS_VIRTUAL;
   2910         }
   2911 
   2912         // If we don't know the protocol or the protocol doesn't support it, don't allow moving
   2913         // messages
   2914         if (info == null || !info.offerMoveTo) {
   2915             caps &= ~UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES &
   2916                     ~UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION &
   2917                     ~UIProvider.FolderCapabilities.ALLOWS_MOVE_TO_INBOX;
   2918         }
   2919         return caps;
   2920     }
   2921 
   2922     /**
   2923      * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
   2924      *
   2925      * @param uiProjection as passed from UnifiedEmail
   2926      * @return the SQLite query to be executed on the EmailProvider database
   2927      */
   2928     private String genQueryMailbox(String[] uiProjection, String id) {
   2929         long mailboxId = Long.parseLong(id);
   2930         ContentValues values = new ContentValues(3);
   2931         if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
   2932             // "load more" is valid for search results
   2933             values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
   2934                     uiUriString("uiloadmore", mailboxId));
   2935             values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE);
   2936         } else {
   2937             Context context = getContext();
   2938             Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
   2939             // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot)
   2940             if (mailbox != null) {
   2941                 String protocol = Account.getProtocol(context, mailbox.mAccountKey);
   2942                 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   2943                 // All folders support delete
   2944                 if (info != null && info.offerLoadMore) {
   2945                     // "load more" is valid for protocols not supporting "lookback"
   2946                     values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
   2947                             uiUriString("uiloadmore", mailboxId));
   2948                 }
   2949                 values.put(UIProvider.FolderColumns.CAPABILITIES,
   2950                         getFolderCapabilities(info, mailbox.mType, mailboxId));
   2951                 // The persistent id is used to form a filename, so we must ensure that it doesn't
   2952                 // include illegal characters (such as '/'). Only perform the encoding if this
   2953                 // query wants the persistent id.
   2954                 boolean shouldEncodePersistentId = false;
   2955                 if (uiProjection == null) {
   2956                     shouldEncodePersistentId = true;
   2957                 } else {
   2958                     for (final String column : uiProjection) {
   2959                         if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) {
   2960                             shouldEncodePersistentId = true;
   2961                             break;
   2962                         }
   2963                     }
   2964                 }
   2965                 if (shouldEncodePersistentId) {
   2966                     values.put(UIProvider.FolderColumns.PERSISTENT_ID,
   2967                             Base64.encodeToString(mailbox.mServerId.getBytes(),
   2968                                     Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
   2969                 }
   2970              }
   2971         }
   2972         StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values);
   2973         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?");
   2974         return sb.toString();
   2975     }
   2976 
   2977     public static final String LEGACY_AUTHORITY = "ui.email.android.com";
   2978     private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY);
   2979 
   2980     private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
   2981 
   2982     private static String getExternalUriString(String segment, String account) {
   2983         return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
   2984                 .appendQueryParameter("account", account).build().toString();
   2985     }
   2986 
   2987     private static String getExternalUriStringEmail2(String segment, String account) {
   2988         return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
   2989                 .appendQueryParameter("account", account).build().toString();
   2990     }
   2991 
   2992     private static String getBits(int bitField) {
   2993         StringBuilder sb = new StringBuilder(" ");
   2994         for (int i = 0; i < 32; i++, bitField >>= 1) {
   2995             if ((bitField & 1) != 0) {
   2996                 sb.append(i)
   2997                         .append(" ");
   2998             }
   2999         }
   3000         return sb.toString();
   3001     }
   3002 
   3003     private static int getCapabilities(Context context, long accountId) {
   3004         final Account account = Account.restoreAccountWithId(context, accountId);
   3005         if (account == null) {
   3006             LogUtils.d(TAG, "Account %d not found during getCapabilities", accountId);
   3007             return 0;
   3008         }
   3009         // Account capabilities are based on protocol -- different protocols (and, for EAS,
   3010         // different protocol versions) support different feature sets.
   3011         final String protocol = account.getProtocol(context);
   3012         int capabilities;
   3013         if (TextUtils.equals(context.getString(R.string.protocol_imap), protocol) ||
   3014                 TextUtils.equals(context.getString(R.string.protocol_legacy_imap), protocol)) {
   3015             capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3016                     AccountCapabilities.FOLDER_SERVER_SEARCH |
   3017                     AccountCapabilities.UNDO |
   3018                     AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3019         } else if (TextUtils.equals(context.getString(R.string.protocol_pop3), protocol)) {
   3020             capabilities = AccountCapabilities.UNDO |
   3021                     AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3022         } else if (TextUtils.equals(context.getString(R.string.protocol_eas), protocol)) {
   3023             final String easVersion = account.mProtocolVersion;
   3024             double easVersionDouble = 2.5D;
   3025             if (easVersion != null) {
   3026                 try {
   3027                     easVersionDouble = Double.parseDouble(easVersion);
   3028                 } catch (final NumberFormatException e) {
   3029                     // Use the default (lowest) set of capabilities.
   3030                 }
   3031             }
   3032             if (easVersionDouble >= 12.0D) {
   3033                 capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3034                         AccountCapabilities.SERVER_SEARCH |
   3035                         AccountCapabilities.FOLDER_SERVER_SEARCH |
   3036                         AccountCapabilities.SMART_REPLY |
   3037                         AccountCapabilities.UNDO |
   3038                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3039             } else {
   3040                 capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
   3041                         AccountCapabilities.SMART_REPLY |
   3042                         AccountCapabilities.UNDO |
   3043                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
   3044             }
   3045         } else {
   3046             LogUtils.w(TAG, "Unknown protocol for account %d", accountId);
   3047             return 0;
   3048         }
   3049         LogUtils.d(TAG, "getCapabilities() for %d (protocol %s): 0x%x %s", accountId, protocol,
   3050                 capabilities, getBits(capabilities));
   3051 
   3052         // If the configuration states that feedback is supported, add that capability
   3053         final Resources res = context.getResources();
   3054         if (res.getBoolean(R.bool.feedback_supported)) {
   3055             capabilities |= UIProvider.AccountCapabilities.SEND_FEEDBACK;
   3056         }
   3057         // TODO: Should this be stored per-account, or some other mechanism?
   3058         capabilities |= UIProvider.AccountCapabilities.NESTED_FOLDERS;
   3059 
   3060         return capabilities;
   3061     }
   3062 
   3063     /**
   3064      * Generate a "single account" SQLite query, given a projection from UnifiedEmail
   3065      *
   3066      * @param uiProjection as passed from UnifiedEmail
   3067      * @param id account row ID
   3068      * @return the SQLite query to be executed on the EmailProvider database
   3069      */
   3070     private String genQueryAccount(String[] uiProjection, String id) {
   3071         final ContentValues values = new ContentValues();
   3072         final long accountId = Long.parseLong(id);
   3073         final Context context = getContext();
   3074 
   3075         EmailServiceInfo info = null;
   3076 
   3077         // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null.
   3078         final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection);
   3079 
   3080         if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) {
   3081             // Get account capabilities from the service
   3082             values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId));
   3083         }
   3084         if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
   3085             values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
   3086                     getExternalUriString("settings", id));
   3087         }
   3088         if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) {
   3089             values.put(UIProvider.AccountColumns.COMPOSE_URI,
   3090                     getExternalUriStringEmail2("compose", id));
   3091         }
   3092         if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) {
   3093             values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE);
   3094         }
   3095         if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) {
   3096             values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR);
   3097         }
   3098 
   3099         final Preferences prefs = Preferences.getPreferences(getContext());
   3100         final MailPrefs mailPrefs = MailPrefs.get(getContext());
   3101         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
   3102             values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE,
   3103                     prefs.getConfirmDelete() ? "1" : "0");
   3104         }
   3105         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
   3106             values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND,
   3107                     prefs.getConfirmSend() ? "1" : "0");
   3108         }
   3109         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) {
   3110             values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE,
   3111                     mailPrefs.getConversationListSwipeActionInteger(false));
   3112         }
   3113         if (projectionColumns.contains(
   3114                 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
   3115             values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON,
   3116                     getConversationListIcon(mailPrefs));
   3117         }
   3118         if (projectionColumns.contains(
   3119                 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) {
   3120             values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS,
   3121                     "0");
   3122         }
   3123         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
   3124             int autoAdvance = prefs.getAutoAdvanceDirection();
   3125             values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE,
   3126                     autoAdvanceToUiValue(autoAdvance));
   3127         }
   3128         if (projectionColumns.contains(
   3129                 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) {
   3130             int textZoom = prefs.getTextZoom();
   3131             values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE,
   3132                     textZoomToUiValue(textZoom));
   3133         }
   3134         // Set default inbox, if we've got an inbox; otherwise, say initial sync needed
   3135         final long inboxMailboxId =
   3136                 Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
   3137         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) &&
   3138                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3139             values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
   3140                     uiUriString("uifolder", inboxMailboxId));
   3141         }
   3142         if (projectionColumns.contains(
   3143                 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) &&
   3144                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3145             values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME,
   3146                     Mailbox.getDisplayName(context, inboxMailboxId));
   3147         }
   3148         if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) {
   3149             if (inboxMailboxId != Mailbox.NO_MAILBOX) {
   3150                 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
   3151             } else {
   3152                 values.put(UIProvider.AccountColumns.SYNC_STATUS,
   3153                         UIProvider.SyncStatus.INITIAL_SYNC_NEEDED);
   3154             }
   3155         }
   3156         if (projectionColumns.contains(
   3157                 UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) {
   3158             // Email doesn't support priority inbox, so always state priority arrows disabled.
   3159             values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0");
   3160         }
   3161         if (projectionColumns.contains(
   3162                 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) {
   3163             // Set the setup intent if needed
   3164             // TODO We should clarify/document the trash/setup relationship
   3165             long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH);
   3166             if (trashId == Mailbox.NO_MAILBOX) {
   3167                 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
   3168                 if (info != null && info.requiresSetup) {
   3169                     values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI,
   3170                             getExternalUriString("setup", id));
   3171                 }
   3172             }
   3173         }
   3174         if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) {
   3175             final String type;
   3176             if (info == null) {
   3177                 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
   3178             }
   3179             if (info != null) {
   3180                 type = info.accountType;
   3181             } else {
   3182                 type = "unknown";
   3183             }
   3184 
   3185             values.put(UIProvider.AccountColumns.TYPE, type);
   3186         }
   3187         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) &&
   3188                 inboxMailboxId != Mailbox.NO_MAILBOX) {
   3189             values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX,
   3190                     uiUriString("uifolder", inboxMailboxId));
   3191         }
   3192         if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) {
   3193             values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY);
   3194         }
   3195         if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) {
   3196             values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI,
   3197                     combinedUriString("quickresponse/account", id));
   3198         }
   3199         if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
   3200             values.put(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR,
   3201                     mailPrefs.getDefaultReplyAll()
   3202                             ? UIProvider.DefaultReplyBehavior.REPLY_ALL
   3203                             : UIProvider.DefaultReplyBehavior.REPLY);
   3204         }
   3205 
   3206         final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values);
   3207         sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
   3208         return sb.toString();
   3209     }
   3210 
   3211     private static int autoAdvanceToUiValue(int autoAdvance) {
   3212         switch(autoAdvance) {
   3213             case Preferences.AUTO_ADVANCE_OLDER:
   3214                 return UIProvider.AutoAdvance.OLDER;
   3215             case Preferences.AUTO_ADVANCE_NEWER:
   3216                 return UIProvider.AutoAdvance.NEWER;
   3217             case Preferences.AUTO_ADVANCE_MESSAGE_LIST:
   3218             default:
   3219                 return UIProvider.AutoAdvance.LIST;
   3220         }
   3221     }
   3222 
   3223     private static int textZoomToUiValue(int textZoom) {
   3224         switch(textZoom) {
   3225             case Preferences.TEXT_ZOOM_HUGE:
   3226                 return UIProvider.MessageTextSize.HUGE;
   3227             case Preferences.TEXT_ZOOM_LARGE:
   3228                 return UIProvider.MessageTextSize.LARGE;
   3229             case Preferences.TEXT_ZOOM_NORMAL:
   3230                 return UIProvider.MessageTextSize.NORMAL;
   3231             case Preferences.TEXT_ZOOM_SMALL:
   3232                 return UIProvider.MessageTextSize.SMALL;
   3233             case Preferences.TEXT_ZOOM_TINY:
   3234                 return UIProvider.MessageTextSize.TINY;
   3235             default:
   3236                 return UIProvider.MessageTextSize.NORMAL;
   3237         }
   3238     }
   3239 
   3240     /**
   3241      * Generate a Uri string for a combined mailbox uri
   3242      * @param type the uri command type (e.g. "uimessages")
   3243      * @param id the id of the item (e.g. an account, mailbox, or message id)
   3244      * @return a Uri string
   3245      */
   3246     private static String combinedUriString(String type, String id) {
   3247         return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id;
   3248     }
   3249 
   3250     public static final long COMBINED_ACCOUNT_ID = 0x10000000;
   3251 
   3252     /**
   3253      * Generate an id for a combined mailbox of a given type
   3254      * @param type the mailbox type for the combined mailbox
   3255      * @return the id, as a String
   3256      */
   3257     private static String combinedMailboxId(int type) {
   3258         return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type);
   3259     }
   3260 
   3261     public static long getVirtualMailboxId(long accountId, int type) {
   3262         return (accountId << 32) + type;
   3263     }
   3264 
   3265     private static boolean isVirtualMailbox(long mailboxId) {
   3266         return mailboxId >= 0x100000000L;
   3267     }
   3268 
   3269     private static boolean isCombinedMailbox(long mailboxId) {
   3270         return (mailboxId >> 32) == COMBINED_ACCOUNT_ID;
   3271     }
   3272 
   3273     private static long getVirtualMailboxAccountId(long mailboxId) {
   3274         return mailboxId >> 32;
   3275     }
   3276 
   3277     private static String getVirtualMailboxAccountIdString(long mailboxId) {
   3278         return Long.toString(mailboxId >> 32);
   3279     }
   3280 
   3281     private static int getVirtualMailboxType(long mailboxId) {
   3282         return (int)(mailboxId & 0xF);
   3283     }
   3284 
   3285     private void addCombinedAccountRow(MatrixCursor mc) {
   3286         final long lastUsedAccountId =
   3287                 Preferences.getPreferences(getContext()).getLastUsedAccountId();
   3288         final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId);
   3289         if (id == Account.NO_ACCOUNT) return;
   3290 
   3291         // Build a map of the requested columns to the appropriate positions
   3292         final ImmutableMap.Builder<String, Integer> builder =
   3293                 new ImmutableMap.Builder<String, Integer>();
   3294         final String[] columnNames = mc.getColumnNames();
   3295         for (int i = 0; i < columnNames.length; i++) {
   3296             builder.put(columnNames[i], i);
   3297         }
   3298         final Map<String, Integer> colPosMap = builder.build();
   3299 
   3300         final MailPrefs mailPrefs = MailPrefs.get(getContext());
   3301 
   3302         final Object[] values = new Object[columnNames.length];
   3303         if (colPosMap.containsKey(BaseColumns._ID)) {
   3304             values[colPosMap.get(BaseColumns._ID)] = 0;
   3305         }
   3306         if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) {
   3307             values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] =
   3308                     AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE;
   3309         }
   3310         if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) {
   3311             values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] =
   3312                     combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING);
   3313         }
   3314         if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) {
   3315             values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString(
   3316                     R.string.mailbox_list_account_selector_combined_view);
   3317         }
   3318         if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)) {
   3319             values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)] =
   3320                     getContext().getString(R.string.mailbox_list_account_selector_combined_view);
   3321         }
   3322         if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) {
   3323             values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown";
   3324         }
   3325         if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) {
   3326             values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] =
   3327                     "'content://" + EmailContent.AUTHORITY + "/uiundo'";
   3328         }
   3329         if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) {
   3330             values[colPosMap.get(UIProvider.AccountColumns.URI)] =
   3331                     combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING);
   3332         }
   3333         if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) {
   3334             values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] =
   3335                     EMAIL_APP_MIME_TYPE;
   3336         }
   3337         if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
   3338             values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] =
   3339                     getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING);
   3340         }
   3341         if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) {
   3342             values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] =
   3343                     getExternalUriStringEmail2("compose", Long.toString(id));
   3344         }
   3345 
   3346         // TODO: Get these from default account?
   3347         Preferences prefs = Preferences.getPreferences(getContext());
   3348         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
   3349             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] =
   3350                     Integer.toString(UIProvider.AutoAdvance.NEWER);
   3351         }
   3352         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) {
   3353             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] =
   3354                     Integer.toString(UIProvider.MessageTextSize.NORMAL);
   3355         }
   3356         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) {
   3357             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] =
   3358                     Integer.toString(UIProvider.SnapHeaderValue.ALWAYS);
   3359         }
   3360         //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
   3361         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
   3362             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] =
   3363                     Integer.toString(mailPrefs.getDefaultReplyAll()
   3364                             ? UIProvider.DefaultReplyBehavior.REPLY_ALL
   3365                             : UIProvider.DefaultReplyBehavior.REPLY);
   3366         }
   3367         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
   3368             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] =
   3369                     getConversationListIcon(mailPrefs);
   3370         }
   3371         if (colPosMap.containsKey(
   3372                 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) {
   3373             values[colPosMap.get(
   3374                     UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)] = 0;
   3375         }
   3376         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
   3377             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] =
   3378                     prefs.getConfirmDelete() ? 1 : 0;
   3379         }
   3380         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) {
   3381             values[colPosMap.get(
   3382                     UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0;
   3383         }
   3384         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
   3385             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] =
   3386                     prefs.getConfirmSend() ? 1 : 0;
   3387         }
   3388         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) {
   3389             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] =
   3390                     combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
   3391         }
   3392         if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) {
   3393             values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] =
   3394                     combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
   3395         }
   3396 
   3397         mc.addRow(values);
   3398     }
   3399 
   3400     private static int getConversationListIcon(MailPrefs mailPrefs) {
   3401         return mailPrefs.getShowSenderImages() ?
   3402                 UIProvider.ConversationListIcon.SENDER_IMAGE :
   3403                 UIProvider.ConversationListIcon.NONE;
   3404     }
   3405 
   3406     // TODO: Pass in projection b/10912870
   3407     private Cursor getVirtualMailboxCursor(long mailboxId) {
   3408         MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1);
   3409         mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId),
   3410                 getVirtualMailboxType(mailboxId)));
   3411         return mc;
   3412     }
   3413 
   3414     // TODO: Pass in projection b/10912870
   3415     private Object[] getVirtualMailboxRow(long accountId, int mailboxType) {
   3416         final long id = getVirtualMailboxId(accountId, mailboxType);
   3417         final String idString = Long.toString(id);
   3418         Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length];
   3419         values[UIProvider.FOLDER_ID_COLUMN] = id;
   3420         values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString);
   3421         values[UIProvider.FOLDER_NAME_COLUMN] = getFolderDisplayName(
   3422                 getFolderTypeFromMailboxType(mailboxType), "");
   3423                 // default empty string since all of these should use resource strings
   3424         values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0;
   3425         values[UIProvider.FOLDER_CAPABILITIES_COLUMN] =
   3426                 UIProvider.FolderCapabilities.DELETE | UIProvider.FolderCapabilities.IS_VIRTUAL;
   3427         values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages",
   3428                 idString);
   3429 
   3430         // Do any special handling
   3431         final String accountIdString = Long.toString(accountId);
   3432         switch (mailboxType) {
   3433             case Mailbox.TYPE_INBOX:
   3434                 if (accountId == COMBINED_ACCOUNT_ID) {
   3435                     // Add the unread count
   3436                     final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3437                             MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID
   3438                             + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE
   3439                             + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0",
   3440                             null);
   3441                     values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount;
   3442                 }
   3443                 // Add the icon
   3444                 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_inbox;
   3445                 break;
   3446             case Mailbox.TYPE_UNREAD: {
   3447                 // Add the unread count
   3448                 final String accountKeyClause;
   3449                 final String[] whereArgs;
   3450                 if (accountId == COMBINED_ACCOUNT_ID) {
   3451                     accountKeyClause = "";
   3452                     whereArgs = null;
   3453                 } else {
   3454                     accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
   3455                     whereArgs = new String[] { accountIdString };
   3456                 }
   3457                 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3458                         accountKeyClause + MessageColumns.FLAG_READ + "=0 AND "
   3459                         + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns.ID
   3460                         + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "="
   3461                         + Mailbox.TYPE_TRASH + ")", whereArgs);
   3462                 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount;
   3463                 // Add the icon
   3464                 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_unread;
   3465                 break;
   3466             } case Mailbox.TYPE_STARRED: {
   3467                 // Add the starred count as the unread count
   3468                 final String accountKeyClause;
   3469                 final String[] whereArgs;
   3470                 if (accountId == COMBINED_ACCOUNT_ID) {
   3471                     accountKeyClause = "";
   3472                     whereArgs = null;
   3473                 } else {
   3474                     accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
   3475                     whereArgs = new String[] { accountIdString };
   3476                 }
   3477                 final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI,
   3478                         accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs);
   3479                 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = starredCount;
   3480                 // Add the icon
   3481                 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_star;
   3482                 break;
   3483             }
   3484         }
   3485 
   3486         return values;
   3487     }
   3488 
   3489     private Cursor uiAccounts(String[] uiProjection) {
   3490         final Context context = getContext();
   3491         final SQLiteDatabase db = getDatabase(context);
   3492         final Cursor accountIdCursor =
   3493                 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
   3494         final MatrixCursor mc;
   3495         try {
   3496             boolean combinedAccount = false;
   3497             if (accountIdCursor.getCount() > 1) {
   3498                 combinedAccount = true;
   3499             }
   3500             final Bundle extras = new Bundle();
   3501             // Email always returns the accurate number of accounts
   3502             extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1);
   3503             mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras);
   3504             final Object[] values = new Object[uiProjection.length];
   3505             while (accountIdCursor.moveToNext()) {
   3506                 final String id = accountIdCursor.getString(0);
   3507                 final Cursor accountCursor =
   3508                         db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
   3509                 try {
   3510                     if (accountCursor.moveToNext()) {
   3511                         for (int i = 0; i < uiProjection.length; i++) {
   3512                             values[i] = accountCursor.getString(i);
   3513                         }
   3514                         mc.addRow(values);
   3515                     }
   3516                 } finally {
   3517                     accountCursor.close();
   3518                 }
   3519             }
   3520             if (combinedAccount) {
   3521                 addCombinedAccountRow(mc);
   3522             }
   3523         } finally {
   3524             accountIdCursor.close();
   3525         }
   3526         mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER);
   3527 
   3528         return mc;
   3529     }
   3530 
   3531     private Cursor uiQuickResponseAccount(String[] uiProjection, String account) {
   3532         final Context context = getContext();
   3533         final SQLiteDatabase db = getDatabase(context);
   3534         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   3535         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   3536         sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?");
   3537         final String query = sb.toString();
   3538         return db.rawQuery(query, new String[] {account});
   3539     }
   3540 
   3541     private Cursor uiQuickResponseId(String[] uiProjection, String id) {
   3542         final Context context = getContext();
   3543         final SQLiteDatabase db = getDatabase(context);
   3544         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   3545         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   3546         sb.append(" WHERE " + QuickResponse.ID + "=?");
   3547         final String query = sb.toString();
   3548         return db.rawQuery(query, new String[] {id});
   3549     }
   3550 
   3551     private Cursor uiQuickResponse(String[] uiProjection) {
   3552         final Context context = getContext();
   3553         final SQLiteDatabase db = getDatabase(context);
   3554         final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
   3555         sb.append(" FROM " + QuickResponse.TABLE_NAME);
   3556         final String query = sb.toString();
   3557         return db.rawQuery(query, new String[0]);
   3558     }
   3559 
   3560     /**
   3561      * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
   3562      *
   3563      * @param uiProjection as passed from UnifiedEmail
   3564      * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments
   3565      * or null if there are no query parameters
   3566      * @return the SQLite query to be executed on the EmailProvider database
   3567      */
   3568     private static String genQueryAttachments(String[] uiProjection,
   3569             List<String> contentTypeQueryParameters) {
   3570         // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT
   3571         ContentValues values = new ContentValues(1);
   3572         values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
   3573         StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
   3574         sb.append(" FROM ")
   3575                 .append(Attachment.TABLE_NAME)
   3576                 .append(" WHERE ")
   3577                 .append(AttachmentColumns.MESSAGE_KEY)
   3578                 .append(" =? ");
   3579 
   3580         // Filter for certain content types.
   3581         // The filter works by adding LIKE operators for each
   3582         // content type you wish to request. Content types
   3583         // are filtered by performing a case-insensitive "starts with"
   3584         // filter. IE, "image/" would return "image/png" as well as "image/jpeg".
   3585         if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
   3586             final int size = contentTypeQueryParameters.size();
   3587             sb.append("AND (");
   3588             for (int i = 0; i < size; i++) {
   3589                 final String contentType = contentTypeQueryParameters.get(i);
   3590                 sb.append(AttachmentColumns.MIME_TYPE)
   3591                         .append(" LIKE '")
   3592                         .append(contentType)
   3593                         .append("%'");
   3594 
   3595                 if (i != size - 1) {
   3596                     sb.append(" OR ");
   3597                 }
   3598             }
   3599             sb.append(")");
   3600         }
   3601         return sb.toString();
   3602     }
   3603 
   3604     /**
   3605      * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
   3606      *
   3607      * @param uiProjection as passed from UnifiedEmail
   3608      * @return the SQLite query to be executed on the EmailProvider database
   3609      */
   3610     private String genQueryAttachment(String[] uiProjection, String idString) {
   3611         Long id = Long.parseLong(idString);
   3612         Attachment att = Attachment.restoreAttachmentWithId(getContext(), id);
   3613         // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS
   3614         ContentValues values = new ContentValues(2);
   3615         values.put(AttachmentColumns.CONTENT_URI,
   3616                 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString());
   3617         values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
   3618         StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
   3619         sb.append(" FROM ")
   3620                 .append(Attachment.TABLE_NAME)
   3621                 .append(" WHERE ")
   3622                 .append(AttachmentColumns.ID)
   3623                 .append(" =? ");
   3624         return sb.toString();
   3625     }
   3626 
   3627     /**
   3628      * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
   3629      *
   3630      * @param uiProjection as passed from UnifiedEmail
   3631      * @return the SQLite query to be executed on the EmailProvider database
   3632      */
   3633     private static String genQuerySubfolders(String[] uiProjection) {
   3634         StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
   3635         sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
   3636                 " =? ORDER BY ");
   3637         sb.append(MAILBOX_ORDER_BY);
   3638         return sb.toString();
   3639     }
   3640 
   3641     private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID);
   3642 
   3643     /**
   3644      * Returns a cursor over all the folders for a specific URI which corresponds to a single
   3645      * account.
   3646      * @param uri uri to query
   3647      * @param uiProjection projection
   3648      * @return query result cursor
   3649      */
   3650     private Cursor uiFolders(final Uri uri, final String[] uiProjection) {
   3651         final Context context = getContext();
   3652         final SQLiteDatabase db = getDatabase(context);
   3653         final String id = uri.getPathSegments().get(1);
   3654 
   3655         final Uri notifyUri =
   3656                 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   3657 
   3658         final Cursor vc = uiVirtualMailboxes(id, uiProjection);
   3659         vc.setNotificationUri(context.getContentResolver(), notifyUri);
   3660         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   3661             return vc;
   3662         } else {
   3663             Cursor c = db.rawQuery(genQueryAccountMailboxes(UIProvider.FOLDERS_PROJECTION),
   3664                     new String[] {id});
   3665             c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
   3666             c.setNotificationUri(context.getContentResolver(), notifyUri);
   3667             Cursor[] cursors = new Cursor[] {vc, c};
   3668             return new MergeCursor(cursors);
   3669         }
   3670     }
   3671 
   3672     private Cursor uiVirtualMailboxes(final String id, final String[] uiProjection) {
   3673         final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
   3674 
   3675         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   3676             mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX));
   3677             mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED));
   3678             mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD));
   3679         } else {
   3680             final long acctId = Long.parseLong(id);
   3681             mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED));
   3682             mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD));
   3683         }
   3684 
   3685         return mc;
   3686     }
   3687 
   3688     /**
   3689      * Returns an array of the default recent folders for a given URI which is unique for an
   3690      * account. Some accounts might not have default recent folders, in which case an empty array
   3691      * is returned.
   3692      * @param id account id
   3693      * @return array of URIs
   3694      */
   3695     private Uri[] defaultRecentFolders(final String id) {
   3696         final SQLiteDatabase db = getDatabase(getContext());
   3697         if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   3698             // We don't have default recents for the combined view.
   3699             return new Uri[0];
   3700         }
   3701         // We search for the types we want, and find corresponding IDs.
   3702         final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE };
   3703 
   3704         // Sent, Drafts, and Starred are the default recents.
   3705         final StringBuilder sb = genSelect(getFolderListMap(), idAndType);
   3706         sb.append(" FROM ")
   3707                 .append(Mailbox.TABLE_NAME)
   3708                 .append(" WHERE ")
   3709                 .append(MailboxColumns.ACCOUNT_KEY)
   3710                 .append(" = ")
   3711                 .append(id)
   3712                 .append(" AND ")
   3713                 .append(MailboxColumns.TYPE)
   3714                 .append(" IN (")
   3715                 .append(Mailbox.TYPE_SENT)
   3716                 .append(", ")
   3717                 .append(Mailbox.TYPE_DRAFTS)
   3718                 .append(", ")
   3719                 .append(Mailbox.TYPE_STARRED)
   3720                 .append(")");
   3721         LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb);
   3722         final Cursor c = db.rawQuery(sb.toString(), null);
   3723         if (c == null || c.getCount() <= 0 || !c.moveToFirst()) {
   3724             return new Uri[0];
   3725         }
   3726         // Read all the IDs of the mailboxes, and turn them into URIs.
   3727         final Uri[] recentFolders = new Uri[c.getCount()];
   3728         int i = 0;
   3729         do {
   3730             final long folderId = c.getLong(0);
   3731             recentFolders[i] = uiUri("uifolder", folderId);
   3732             LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]);
   3733             ++i;
   3734         } while (c.moveToNext());
   3735         return recentFolders;
   3736     }
   3737 
   3738     /**
   3739      * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so
   3740      * any pending notifications for the corresponding mailbox should be canceled). We also handle
   3741      * getExtras() to provide a snapshot of the mailbox's status
   3742      */
   3743     static class EmailConversationCursor extends CursorWrapper {
   3744         private final long mMailboxId;
   3745         private final Context mContext;
   3746         private final FolderList mFolderList;
   3747         private final Bundle mExtras = new Bundle();
   3748 
   3749         /**
   3750          * When showing a folder, if it's been at least this long since the last sync,
   3751          * force a folder refresh.
   3752          */
   3753         private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS;
   3754 
   3755         public EmailConversationCursor(final Context context, final Cursor cursor,
   3756                 final Folder folder, final long mailboxId) {
   3757             super(cursor);
   3758             mMailboxId = mailboxId;
   3759             mContext = context;
   3760             mFolderList = FolderList.copyOf(Lists.newArrayList(folder));
   3761             Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
   3762 
   3763             if (mailbox != null) {
   3764                 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR,
   3765                         mailbox.mUiLastSyncResult);
   3766                 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount);
   3767                  if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_BACKGROUND
   3768                          || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_USER
   3769                          || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_LIVE) {
   3770                     mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
   3771                             UIProvider.CursorStatus.LOADING);
   3772                 } else if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_NONE) {
   3773                      if (mailbox.mSyncInterval == 0
   3774                              && (Mailbox.isSyncableType(mailbox.mType)
   3775                                     || mailbox.mType == Mailbox.TYPE_SEARCH)
   3776                              && !TextUtils.isEmpty(mailbox.mServerId) &&
   3777                              // TODO: There's potentially a race condition here.
   3778                              // Consider merging this check with the auto-sync code in respond.
   3779                              System.currentTimeMillis() - mailbox.mSyncTime
   3780                                      > AUTO_REFRESH_INTERVAL_MS) {
   3781                          // This will be syncing momentarily
   3782                          mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
   3783                                  UIProvider.CursorStatus.LOADING);
   3784                      } else {
   3785                          mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
   3786                                  UIProvider.CursorStatus.COMPLETE);
   3787                      }
   3788                  } else {
   3789                      LogUtils.d(Logging.LOG_TAG,
   3790                              "Unknown mailbox sync status" + mailbox.mUiSyncStatus);
   3791                      mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
   3792                              UIProvider.CursorStatus.COMPLETE);
   3793                  }
   3794             } else {
   3795                 // TODO for virtual mailboxes, we may want to do something besides just fake it
   3796                 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR,
   3797                         UIProvider.LastSyncResult.SUCCESS);
   3798                 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT,
   3799                         cursor != null ? cursor.getCount() : 0);
   3800                 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
   3801                         UIProvider.CursorStatus.COMPLETE);
   3802             }
   3803         }
   3804 
   3805         @Override
   3806         public Bundle getExtras() {
   3807             return mExtras;
   3808         }
   3809 
   3810         @Override
   3811         public Bundle respond(Bundle params) {
   3812             final String setVisibilityKey =
   3813                     UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY;
   3814             if (params.containsKey(setVisibilityKey)) {
   3815                 final boolean visible = params.getBoolean(setVisibilityKey);
   3816                 if (visible) {
   3817                     // Mark all messages as seen
   3818                     final ContentResolver resolver = mContext.getContentResolver();
   3819                     final ContentValues contentValues = new ContentValues(1);
   3820                     contentValues.put(MessageColumns.FLAG_SEEN, true);
   3821                     final Uri uri = EmailContent.Message.CONTENT_URI;
   3822                     resolver.update(uri, contentValues, MessageColumns.MAILBOX_KEY + " = ?",
   3823                             new String[] {String.valueOf(mMailboxId)});
   3824                     if (params.containsKey(
   3825                             UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) {
   3826                         Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
   3827                         if (mailbox != null) {
   3828                             // For non-push mailboxes, if it's stale (i.e. last sync was a while
   3829                             // ago), force a sync.
   3830                             // TODO: Fix the check for whether we're non-push? Right now it checks
   3831                             // whether we are participating in account sync rules.
   3832                             if (mailbox.mSyncInterval == 0) {
   3833                                 final long timeSinceLastSync =
   3834                                         System.currentTimeMillis() - mailbox.mSyncTime;
   3835                                 if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) {
   3836                                     final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI +
   3837                                             "/" + QUERY_UIREFRESH + "/" + mailbox.mId);
   3838                                     resolver.query(refreshUri, null, null, null, null);
   3839                                 }
   3840                             }
   3841                         }
   3842                     }
   3843                 }
   3844             }
   3845             // Return success
   3846             final Bundle response = new Bundle(2);
   3847 
   3848             response.putString(setVisibilityKey,
   3849                     UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK);
   3850 
   3851             final String rawFoldersKey =
   3852                     UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS;
   3853             if (params.containsKey(rawFoldersKey)) {
   3854                 response.putParcelable(rawFoldersKey, mFolderList);
   3855             }
   3856 
   3857             final String convInfoKey =
   3858                     UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO;
   3859             if (params.containsKey(convInfoKey)) {
   3860                 response.putParcelable(convInfoKey, generateConversationInfo());
   3861             }
   3862 
   3863             return response;
   3864         }
   3865 
   3866         private ConversationInfo generateConversationInfo() {
   3867             final int numMessages = getInt(getColumnIndex(ConversationColumns.NUM_MESSAGES));
   3868             final ConversationInfo conversationInfo = new ConversationInfo(numMessages);
   3869 
   3870             conversationInfo.firstSnippet = getString(getColumnIndex(ConversationColumns.SNIPPET));
   3871 
   3872             final boolean isRead = getInt(getColumnIndex(ConversationColumns.READ)) != 0;
   3873             final boolean isStarred = getInt(getColumnIndex(ConversationColumns.STARRED)) != 0;
   3874             final String senderString = getString(getColumnIndex(MessageColumns.DISPLAY_NAME));
   3875 
   3876             final String fromString = getString(getColumnIndex(MessageColumns.FROM_LIST));
   3877             final String email;
   3878 
   3879             if (fromString != null) {
   3880                 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(fromString);
   3881                 if (tokens.length > 0) {
   3882                     email = tokens[0].getAddress();
   3883                 } else {
   3884                     LogUtils.d(TAG, "Couldn't parse email address");
   3885                     email = fromString;
   3886                 }
   3887             } else {
   3888                 email = null;
   3889             }
   3890 
   3891             final MessageInfo messageInfo = new MessageInfo(isRead, isStarred, senderString,
   3892                     0 /* priority */, email);
   3893             conversationInfo.addMessage(messageInfo);
   3894 
   3895             return conversationInfo;
   3896         }
   3897     }
   3898 
   3899     /**
   3900      * Convenience method to create a {@link Folder}
   3901      * @param context to get a {@link ContentResolver}
   3902      * @param mailboxId id of the {@link Mailbox} that we want
   3903      * @return the {@link Folder} or null
   3904      */
   3905     public static Folder getFolder(Context context, long mailboxId) {
   3906         final ContentResolver resolver = context.getContentResolver();
   3907         final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId),
   3908                 UIProvider.FOLDERS_PROJECTION, null, null, null);
   3909 
   3910         if (fc == null) {
   3911             LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId);
   3912             return null;
   3913         }
   3914 
   3915         Folder uiFolder = null;
   3916         try {
   3917             if (fc.moveToFirst()) {
   3918                 uiFolder = new Folder(fc);
   3919             }
   3920         } finally {
   3921             fc.close();
   3922         }
   3923         return uiFolder;
   3924     }
   3925 
   3926     static class AttachmentsCursor extends CursorWrapper {
   3927         private final int mContentUriIndex;
   3928         private final int mUriIndex;
   3929         private final Context mContext;
   3930 
   3931         public AttachmentsCursor(Context context, Cursor cursor) {
   3932             super(cursor);
   3933             mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI);
   3934             mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI);
   3935             mContext = context;
   3936         }
   3937 
   3938         @Override
   3939         public String getString(int column) {
   3940             if (column == mContentUriIndex) {
   3941                 final Uri uri = Uri.parse(getString(mUriIndex));
   3942                 final long id = Long.parseLong(uri.getLastPathSegment());
   3943                 final Attachment att = Attachment.restoreAttachmentWithId(mContext, id);
   3944                 if (att == null) return "";
   3945                 if (!TextUtils.isEmpty(att.getCachedFileUri())) {
   3946                     return att.getCachedFileUri();
   3947                 }
   3948 
   3949                 final String contentUri;
   3950                 // Until the package installer can handle opening apks from a content:// uri, for
   3951                 // any apk that was successfully saved in external storage, return the
   3952                 // content uri from the attachment
   3953                 if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL &&
   3954                         att.mUiState == UIProvider.AttachmentState.SAVED &&
   3955                         TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) {
   3956                     contentUri = att.getContentUri();
   3957                 } else {
   3958                     contentUri =
   3959                             AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString();
   3960                 }
   3961                 return contentUri;
   3962 
   3963             } else {
   3964                 return super.getString(column);
   3965             }
   3966         }
   3967     }
   3968 
   3969     /**
   3970      * For debugging purposes; shouldn't be used in production code
   3971      */
   3972     @SuppressWarnings("unused")
   3973     static class CloseDetectingCursor extends CursorWrapper {
   3974 
   3975         public CloseDetectingCursor(Cursor cursor) {
   3976             super(cursor);
   3977         }
   3978 
   3979         @Override
   3980         public void close() {
   3981             super.close();
   3982             LogUtils.d(TAG, "Closing cursor", new Error());
   3983         }
   3984     }
   3985 
   3986     /**
   3987      * Converts a mailbox in a row of the mailboxCursor into a row
   3988      * in the supplied {@link MatrixCursor} in the format required for {@link Folder}.
   3989      * As a convenience, the modified {@link MatrixCursor} is also returned.
   3990      * @param mc the {@link MatrixCursor} into which the mailbox data will be converted
   3991      * @param projectionLength the length of the projection for this Cursor
   3992      * @param mailboxCursor the cursor supplying the mailbox data
   3993      * @param nameColumn column in the cursor containing the folder name value
   3994      * @param typeColumn column in the cursor containing the folder type value
   3995      * @return the {@link MatrixCursor} containing the transformed data.
   3996      */
   3997     private Cursor getUiFolderCursorRowFromMailboxCursorRow(
   3998             MatrixCursor mc, int projectionLength, Cursor mailboxCursor,
   3999             int nameColumn, int typeColumn) {
   4000         final MatrixCursor.RowBuilder builder = mc.newRow();
   4001         for (int i = 0; i < projectionLength; i++) {
   4002             // If we are at the name column, get the type
   4003             // and use it to use a properly translated string
   4004             // from resources instead of the display name.
   4005             // This ignores display names for system mailboxes.
   4006             if (nameColumn == i) {
   4007                 // We implicitly assume that if name is requested,
   4008                 // type has also been requested. If not, this will
   4009                 // error in unknown ways.
   4010                 final int type = mailboxCursor.getInt(typeColumn);
   4011                 builder.add(getFolderDisplayName(type, mailboxCursor.getString(i)));
   4012             } else {
   4013                 builder.add(mailboxCursor.getString(i));
   4014             }
   4015         }
   4016         return mc;
   4017     }
   4018 
   4019     /**
   4020      * Takes a uifolder cursor (that was generated with a full projection) and remaps values for
   4021      * columns that are difficult to generate in the SQL query. This currently includes:
   4022      * - Folder name (due to system folder localization).
   4023      * - Capabilities (due to this varying by account protocol).
   4024      * - Persistent id (due to needing to base64 encode it).
   4025      * - Load more uri (due to this varying by account protocol).
   4026      * TODO: This would be better as a CursorWrapper, rather than doing a copy.
   4027      * @param inputCursor A cursor containing all columns of {@link UIProvider.FolderColumns}.
   4028      *                    Strictly speaking doesn't need all, but simpler if we assume that.
   4029      * @param outputCursor A MatrixCursor which this function will populate.
   4030      * @param accountId The account id for the mailboxes in this query.
   4031      * @param uiProjection The projection specified by the query.
   4032      */
   4033     private void remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor,
   4034             final long accountId, final String[] uiProjection) {
   4035         // Return early if our input cursor is empty.
   4036         if (inputCursor == null || inputCursor.getCount() == 0) {
   4037             return;
   4038         }
   4039         // Get the column indices for the columns we need during remapping.
   4040         // While we currently could assume the column indicies for UIProvider.FOLDERS_PROJECTION
   4041         // and therefore avoid the calls to getColumnIndex, this at least tries to future-proof a
   4042         // bit.
   4043         // Note that id and type MUST be present for this function to work correctly.
   4044         final int idColumn = inputCursor.getColumnIndex(BaseColumns._ID);
   4045         final int typeColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.TYPE);
   4046         final int nameColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.NAME);
   4047         final int capabilitiesColumn =
   4048                 inputCursor.getColumnIndex(UIProvider.FolderColumns.CAPABILITIES);
   4049         final int persistentIdColumn =
   4050                 inputCursor.getColumnIndex(UIProvider.FolderColumns.PERSISTENT_ID);
   4051         final int loadMoreUriColumn =
   4052                 inputCursor.getColumnIndex(UIProvider.FolderColumns.LOAD_MORE_URI);
   4053 
   4054         // Get the EmailServiceInfo for the current account.
   4055         final Context context = getContext();
   4056         final String protocol = Account.getProtocol(context, accountId);
   4057         final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   4058 
   4059         // Build the return cursor. We iterate over all rows of the input cursor and construct
   4060         // a row in the output using the columns in uiProjection.
   4061         while (inputCursor.moveToNext()) {
   4062             final MatrixCursor.RowBuilder builder = outputCursor.newRow();
   4063             final int type = inputCursor.getInt(typeColumn);
   4064             for (int i = 0; i < uiProjection.length; i++) {
   4065                 // Find the index in the input cursor corresponding the column requested in the
   4066                 // output projection.
   4067                 final int index = inputCursor.getColumnIndex(uiProjection[i]);
   4068                 if (index == -1) {
   4069                     // We don't have this value, so put a blank in the output and move on.
   4070                     builder.add(null);
   4071                     continue;
   4072                 }
   4073                 final String value = inputCursor.getString(index);
   4074                 // remapped indicates whether we've written a value to the output for this column.
   4075                 final boolean remapped;
   4076                 if (nameColumn == index) {
   4077                     // Remap folder name for system folders.
   4078                     builder.add(getFolderDisplayName(type, value));
   4079                     remapped = true;
   4080                 } else if (capabilitiesColumn == index) {
   4081                     // Get the correct capabilities for this folder.
   4082                     builder.add(getFolderCapabilities(info, type, inputCursor.getLong(idColumn)));
   4083                     remapped = true;
   4084                 } else if (persistentIdColumn == index) {
   4085                     // Hash the persistent id.
   4086                     builder.add(Base64.encodeToString(value.getBytes(),
   4087                             Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
   4088                     remapped = true;
   4089                 } else if (loadMoreUriColumn == index && type != Mailbox.TYPE_SEARCH &&
   4090                         (info == null || !info.offerLoadMore)) {
   4091                     // Blank the load more uri for account types that don't offer it.
   4092                     // Note that all account types permit load more for search results.
   4093                     builder.add(null);
   4094                     remapped = true;
   4095                 } else {
   4096                     remapped = false;
   4097                 }
   4098                 // If the above logic didn't write some other value to the output, use the value
   4099                 // from the input cursor.
   4100                 if (!remapped) {
   4101                     builder.add(value);
   4102                 }
   4103             }
   4104         }
   4105     }
   4106 
   4107     private Cursor getFolderListCursor(final Cursor inputCursor, final long accountId,
   4108             final String[] uiProjection) {
   4109         final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
   4110         if (inputCursor != null) {
   4111             try {
   4112                 remapFolderCursor(inputCursor, mc, accountId, uiProjection);
   4113             } finally {
   4114                 inputCursor.close();
   4115             }
   4116         }
   4117         return mc;
   4118     }
   4119 
   4120     /**
   4121      * Returns a {@link String} from Resources corresponding
   4122      * to the {@link UIProvider.FolderType} requested.
   4123      * @param folderType {@link UIProvider.FolderType} value for the folder
   4124      * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType}
   4125      *                    provided is not a system folder.
   4126      * @return a {@link String} to use as the display name for the folder
   4127      */
   4128     private String getFolderDisplayName(int folderType, String defaultName) {
   4129         final int resId;
   4130         switch (folderType) {
   4131             case UIProvider.FolderType.INBOX:
   4132                 resId = R.string.mailbox_name_display_inbox;
   4133                 break;
   4134             case UIProvider.FolderType.OUTBOX:
   4135                 resId = R.string.mailbox_name_display_outbox;
   4136                 break;
   4137             case UIProvider.FolderType.DRAFT:
   4138                 resId = R.string.mailbox_name_display_drafts;
   4139                 break;
   4140             case UIProvider.FolderType.TRASH:
   4141                 resId = R.string.mailbox_name_display_trash;
   4142                 break;
   4143             case UIProvider.FolderType.SENT:
   4144                 resId = R.string.mailbox_name_display_sent;
   4145                 break;
   4146             case UIProvider.FolderType.SPAM:
   4147                 resId = R.string.mailbox_name_display_junk;
   4148                 break;
   4149             case UIProvider.FolderType.STARRED:
   4150                 resId = R.string.mailbox_name_display_starred;
   4151                 break;
   4152             case UIProvider.FolderType.UNREAD:
   4153                 resId = R.string.mailbox_name_display_unread;
   4154                 break;
   4155             default:
   4156                 return defaultName;
   4157         }
   4158         return getContext().getString(resId);
   4159     }
   4160 
   4161     /**
   4162      * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType}
   4163      * equivalent.
   4164      * @param mailboxType a {@link Mailbox} type
   4165      * @return a {@link UIProvider.FolderType} value
   4166      */
   4167     private static int getFolderTypeFromMailboxType(int mailboxType) {
   4168         switch (mailboxType) {
   4169             case Mailbox.TYPE_INBOX:
   4170                 return UIProvider.FolderType.INBOX;
   4171             case Mailbox.TYPE_OUTBOX:
   4172                 return UIProvider.FolderType.OUTBOX;
   4173             case Mailbox.TYPE_DRAFTS:
   4174                 return UIProvider.FolderType.DRAFT;
   4175             case Mailbox.TYPE_TRASH:
   4176                 return UIProvider.FolderType.TRASH;
   4177             case Mailbox.TYPE_SENT:
   4178                 return UIProvider.FolderType.SENT;
   4179             case Mailbox.TYPE_JUNK:
   4180                 return UIProvider.FolderType.SPAM;
   4181             case Mailbox.TYPE_STARRED:
   4182                 return UIProvider.FolderType.STARRED;
   4183             case Mailbox.TYPE_UNREAD:
   4184                 return UIProvider.FolderType.UNREAD;
   4185             case Mailbox.TYPE_SEARCH:
   4186                 // TODO Can the DEFAULT type be removed from SEARCH folders?
   4187                 return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH;
   4188             default:
   4189                 return UIProvider.FolderType.DEFAULT;
   4190         }
   4191     }
   4192 
   4193     /**
   4194      * We need a reasonably full projection for getFolderListCursor to work, but don't always want
   4195      * to do the subquery needed for FolderColumns.UNREAD_SENDERS
   4196      * @param uiProjection The projection we actually want
   4197      * @return Full projection, possibly with or without FolderColumns.UNREAD_SENDERS
   4198      */
   4199     private String[] folderProjectionFromUiProjection(final String[] uiProjection) {
   4200         final Set<String> columns = ImmutableSet.copyOf(uiProjection);
   4201         final String[] folderProjection;
   4202         if (columns.contains(UIProvider.FolderColumns.UNREAD_SENDERS)) {
   4203             return UIProvider.FOLDERS_PROJECTION_WITH_UNREAD_SENDERS;
   4204         } else {
   4205             return UIProvider.FOLDERS_PROJECTION;
   4206         }
   4207     }
   4208 
   4209     /**
   4210      * Handle UnifiedEmail queries here (dispatched from query())
   4211      *
   4212      * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
   4213      * @param uri the original uri passed in from UnifiedEmail
   4214      * @param uiProjection the projection passed in from UnifiedEmail
   4215      * @param unseenOnly <code>true</code> to only return unseen messages (where supported)
   4216      * @return the result Cursor
   4217      */
   4218     private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) {
   4219         Context context = getContext();
   4220         ContentResolver resolver = context.getContentResolver();
   4221         SQLiteDatabase db = getDatabase(context);
   4222         // Should we ever return null, or throw an exception??
   4223         Cursor c = null;
   4224         String id = uri.getPathSegments().get(1);
   4225         Uri notifyUri = null;
   4226         switch(match) {
   4227             case UI_ALL_FOLDERS:
   4228                 notifyUri =
   4229                         UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   4230                 final Cursor vc = uiVirtualMailboxes(id, uiProjection);
   4231                 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4232                     // There's no real mailboxes, so just return the virtual ones
   4233                     c = vc;
   4234                 } else {
   4235                     // Return real and virtual mailboxes alike
   4236                     final Cursor rawc = db.rawQuery(genQueryAccountAllMailboxes(uiProjection),
   4237                             new String[] {id});
   4238                     rawc.setNotificationUri(context.getContentResolver(), notifyUri);
   4239                     vc.setNotificationUri(context.getContentResolver(), notifyUri);
   4240                     c = new MergeCursor(new Cursor[] {rawc, vc});
   4241                 }
   4242                 break;
   4243             case UI_FULL_FOLDERS: {
   4244                 // We need a full projection for getFolderListCursor
   4245                 final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
   4246                 c = db.rawQuery(genQueryAccountAllMailboxes(folderProjection), new String[] {id});
   4247                 c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
   4248                 notifyUri =
   4249                         UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
   4250                 break;
   4251             }
   4252             case UI_RECENT_FOLDERS:
   4253                 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id});
   4254                 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
   4255                 break;
   4256             case UI_SUBFOLDERS: {
   4257                 // We need a full projection for getFolderListCursor
   4258                 final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
   4259                 c = db.rawQuery(genQuerySubfolders(folderProjection), new String[] {id});
   4260                 c = getFolderListCursor(c, Mailbox.getAccountIdForMailbox(context, id),
   4261                         uiProjection);
   4262                 // Get notifications for any folder changes on this account. This is broader than
   4263                 // we need but otherwise we'd need for every folder change to notify on all relevant
   4264                 // subtrees. For now we opt for simplicity.
   4265                 final long accountId = Mailbox.getAccountIdForMailbox(context, id);
   4266                 notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
   4267                 break;
   4268             }
   4269             case UI_MESSAGES:
   4270                 long mailboxId = Long.parseLong(id);
   4271                 final Folder folder = getFolder(context, mailboxId);
   4272                 if (folder == null) {
   4273                     // This mailboxId is bogus. Return an empty cursor
   4274                     // TODO: Make callers of this query handle null cursors instead b/10819309
   4275                     return new MatrixCursor(uiProjection);
   4276                 }
   4277                 if (isVirtualMailbox(mailboxId)) {
   4278                     c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly);
   4279                 } else {
   4280                     c = db.rawQuery(
   4281                             genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id});
   4282                 }
   4283                 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
   4284                 c = new EmailConversationCursor(context, c, folder, mailboxId);
   4285                 break;
   4286             case UI_MESSAGE:
   4287                 MessageQuery qq = genQueryViewMessage(uiProjection, id);
   4288                 String sql = qq.query;
   4289                 String attJson = qq.attachmentJson;
   4290                 // With attachments, we have another argument to bind
   4291                 if (attJson != null) {
   4292                     c = db.rawQuery(sql, new String[] {attJson, id});
   4293                 } else {
   4294                     c = db.rawQuery(sql, new String[] {id});
   4295                 }
   4296                 notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();
   4297                 break;
   4298             case UI_ATTACHMENTS:
   4299                 final List<String> contentTypeQueryParameters =
   4300                         uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
   4301                 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters),
   4302                         new String[] {id});
   4303                 c = new AttachmentsCursor(context, c);
   4304                 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
   4305                 break;
   4306             case UI_ATTACHMENT:
   4307                 c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id});
   4308                 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
   4309                 break;
   4310             case UI_FOLDER:
   4311                 mailboxId = Long.parseLong(id);
   4312                 if (isVirtualMailbox(mailboxId)) {
   4313                     c = getVirtualMailboxCursor(mailboxId);
   4314                     notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build();
   4315                 } else {
   4316                     c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[]{id});
   4317                     final List<String> projectionList = Arrays.asList(uiProjection);
   4318                     final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME);
   4319                     final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE);
   4320                     if (c.moveToFirst()) {
   4321                         c = getUiFolderCursorRowFromMailboxCursorRow(
   4322                                 new MatrixCursorWithCachedColumns(uiProjection),
   4323                                 uiProjection.length, c, nameColumn, typeColumn);
   4324                     }
   4325                     notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build();
   4326                 }
   4327                 break;
   4328             case UI_ACCOUNT:
   4329                 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
   4330                     MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1);
   4331                     addCombinedAccountRow(mc);
   4332                     c = mc;
   4333                 } else {
   4334                     c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
   4335                 }
   4336                 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
   4337                 break;
   4338             case UI_CONVERSATION:
   4339                 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id});
   4340                 break;
   4341         }
   4342         if (notifyUri != null) {
   4343             c.setNotificationUri(resolver, notifyUri);
   4344         }
   4345         return c;
   4346     }
   4347 
   4348     /**
   4349      * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need
   4350      * a few of the fields
   4351      * @param uiAtt the UIProvider attachment to convert
   4352      * @param cachedFile the path to the cached file to
   4353      * @return the EmailProvider attachment
   4354      */
   4355     // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be
   4356     // removed
   4357     // TODO(mhibdon): if the UI Attachment contained the account key, the third parameter could
   4358     // be removed.
   4359     private static Attachment convertUiAttachmentToAttachment(
   4360             com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey) {
   4361         final Attachment att = new Attachment();
   4362 
   4363         att.setContentUri(uiAtt.contentUri.toString());
   4364 
   4365         if (!TextUtils.isEmpty(cachedFile)) {
   4366             // Generate the content provider uri for this cached file
   4367             final Uri.Builder cachedFileBuilder = Uri.parse(
   4368                     "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon();
   4369             cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile);
   4370             att.setCachedFileUri(cachedFileBuilder.build().toString());
   4371         }
   4372         att.mAccountKey = accountKey;
   4373         att.mFileName = uiAtt.getName();
   4374         att.mMimeType = uiAtt.getContentType();
   4375         att.mSize = uiAtt.size;
   4376         return att;
   4377     }
   4378 
   4379     /**
   4380      * Create a mailbox given the account and mailboxType.
   4381      */
   4382     private Mailbox createMailbox(long accountId, int mailboxType) {
   4383         Context context = getContext();
   4384         Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType);
   4385         // Make sure drafts and save will show up in recents...
   4386         // If these already exist (from old Email app), they will have touch times
   4387         switch (mailboxType) {
   4388             case Mailbox.TYPE_DRAFTS:
   4389                 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
   4390                 break;
   4391             case Mailbox.TYPE_SENT:
   4392                 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
   4393                 break;
   4394         }
   4395         box.save(context);
   4396         return box;
   4397     }
   4398 
   4399     /**
   4400      * Given an account name and a mailbox type, return that mailbox, creating it if necessary
   4401      * @param accountId the account id to use
   4402      * @param mailboxType the type of mailbox we're trying to find
   4403      * @return the mailbox of the given type for the account in the uri, or null if not found
   4404      */
   4405     private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) {
   4406         Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType);
   4407         if (mailbox == null) {
   4408             mailbox = createMailbox(accountId, mailboxType);
   4409         }
   4410         return mailbox;
   4411     }
   4412 
   4413     /**
   4414      * Given a mailbox and the content values for a message, create/save the message in the mailbox
   4415      * @param mailbox the mailbox to use
   4416      * @param extras the bundle containing the message fields
   4417      * @return the uri of the newly created message
   4418      * TODO(yph): The following fields are available in extras but unused, verify whether they
   4419      *     should be respected:
   4420      *     - UIProvider.MessageColumns.SNIPPET
   4421      *     - UIProvider.MessageColumns.REPLY_TO
   4422      *     - UIProvider.MessageColumns.FROM
   4423      */
   4424     private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) {
   4425         final Context context = getContext();
   4426         // Fill in the message
   4427         final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
   4428         if (account == null) return null;
   4429         final String customFromAddress =
   4430                 extras.getString(UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS);
   4431         if (!TextUtils.isEmpty(customFromAddress)) {
   4432             msg.mFrom = customFromAddress;
   4433         } else {
   4434             msg.mFrom = account.getEmailAddress();
   4435         }
   4436         msg.mTimeStamp = System.currentTimeMillis();
   4437         msg.mTo = extras.getString(UIProvider.MessageColumns.TO);
   4438         msg.mCc = extras.getString(UIProvider.MessageColumns.CC);
   4439         msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC);
   4440         msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT);
   4441         msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT);
   4442         msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML);
   4443         msg.mMailboxKey = mailbox.mId;
   4444         msg.mAccountKey = mailbox.mAccountKey;
   4445         msg.mDisplayName = msg.mTo;
   4446         msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
   4447         msg.mFlagRead = true;
   4448         msg.mFlagSeen = true;
   4449         final Integer quoteStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
   4450         msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos;
   4451         int flags = 0;
   4452         final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE);
   4453         switch(draftType) {
   4454             case DraftType.FORWARD:
   4455                 flags |= Message.FLAG_TYPE_FORWARD;
   4456                 break;
   4457             case DraftType.REPLY_ALL:
   4458                 flags |= Message.FLAG_TYPE_REPLY_ALL;
   4459                 //$FALL-THROUGH$
   4460             case DraftType.REPLY:
   4461                 flags |= Message.FLAG_TYPE_REPLY;
   4462                 break;
   4463             case DraftType.COMPOSE:
   4464                 flags |= Message.FLAG_TYPE_ORIGINAL;
   4465                 break;
   4466         }
   4467         int draftInfo = 0;
   4468         if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) {
   4469             draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
   4470             if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) {
   4471                 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE;
   4472             }
   4473         }
   4474         if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) {
   4475             flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT;
   4476         }
   4477         msg.mDraftInfo = draftInfo;
   4478         msg.mFlags = flags;
   4479 
   4480         final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID);
   4481         if (ref != null && msg.mQuotedTextStartPos >= 0) {
   4482             String refId = Uri.parse(ref).getLastPathSegment();
   4483             try {
   4484                 msg.mSourceKey = Long.parseLong(refId);
   4485             } catch (NumberFormatException e) {
   4486                 // This will be zero; the default
   4487             }
   4488         }
   4489 
   4490         // Get attachments from the ContentValues
   4491         final List<com.android.mail.providers.Attachment> uiAtts =
   4492                 com.android.mail.providers.Attachment.fromJSONArray(
   4493                         extras.getString(UIProvider.MessageColumns.ATTACHMENTS));
   4494         final ArrayList<Attachment> atts = new ArrayList<Attachment>();
   4495         boolean hasUnloadedAttachments = false;
   4496         Bundle attachmentFds =
   4497                 extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP);
   4498         for (com.android.mail.providers.Attachment uiAtt: uiAtts) {
   4499             final Uri attUri = uiAtt.uri;
   4500             if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) {
   4501                 // If it's one of ours, retrieve the attachment and add it to the list
   4502                 final long attId = Long.parseLong(attUri.getLastPathSegment());
   4503                 final Attachment att = Attachment.restoreAttachmentWithId(context, attId);
   4504                 if (att != null) {
   4505                     // We must clone the attachment into a new one for this message; easiest to
   4506                     // use a parcel here
   4507                     final Parcel p = Parcel.obtain();
   4508                     att.writeToParcel(p, 0);
   4509                     p.setDataPosition(0);
   4510                     final Attachment attClone = new Attachment(p);
   4511                     p.recycle();
   4512                     // Clear the messageKey (this is going to be a new attachment)
   4513                     attClone.mMessageKey = 0;
   4514                     // If we're sending this, it's not loaded, and we're not smart forwarding
   4515                     // add the download flag, so that ADS will start up
   4516                     if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null &&
   4517                             ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
   4518                         attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD;
   4519                         hasUnloadedAttachments = true;
   4520                     }
   4521                     atts.add(attClone);
   4522                 }
   4523             } else {
   4524                 // Cache the attachment.  This will allow us to send it, if the permissions are
   4525                 // revoked.
   4526                 final String cachedFileUri =
   4527                         AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds);
   4528 
   4529                 // Convert external attachment to one of ours and add to the list
   4530                 atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri, msg.mAccountKey));
   4531             }
   4532         }
   4533         if (!atts.isEmpty()) {
   4534             msg.mAttachments = atts;
   4535             msg.mFlagAttachment = true;
   4536             if (hasUnloadedAttachments) {
   4537                 Utility.showToast(context, R.string.message_view_attachment_background_load);
   4538             }
   4539         }
   4540         // Save it or update it...
   4541         if (!msg.isSaved()) {
   4542             msg.save(context);
   4543         } else {
   4544             // This is tricky due to how messages/attachments are saved; rather than putz with
   4545             // what's changed, we'll delete/re-add them
   4546             final ArrayList<ContentProviderOperation> ops =
   4547                     new ArrayList<ContentProviderOperation>();
   4548             // Delete all existing attachments
   4549             ops.add(ContentProviderOperation.newDelete(
   4550                     ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId))
   4551                     .build());
   4552             // Delete the body
   4553             ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI)
   4554                     .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)})
   4555                     .build());
   4556             // Add the ops for the message, atts, and body
   4557             msg.addSaveOps(ops);
   4558             // Do it!
   4559             try {
   4560                 applyBatch(ops);
   4561             } catch (OperationApplicationException e) {
   4562                 LogUtils.d(TAG, "applyBatch exception");
   4563             }
   4564         }
   4565         notifyUIMessage(msg.mId);
   4566 
   4567         if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
   4568             startSync(mailbox, 0);
   4569             final long originalMsgId = msg.mSourceKey;
   4570             if (originalMsgId != 0) {
   4571                 final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId);
   4572                 // If the original message exists, set its forwarded/replied to flags
   4573                 if (originalMsg != null) {
   4574                     final ContentValues cv = new ContentValues();
   4575                     flags = originalMsg.mFlags;
   4576                     switch(draftType) {
   4577                         case DraftType.FORWARD:
   4578                             flags |= Message.FLAG_FORWARDED;
   4579                             break;
   4580                         case DraftType.REPLY_ALL:
   4581                         case DraftType.REPLY:
   4582                             flags |= Message.FLAG_REPLIED_TO;
   4583                             break;
   4584                     }
   4585                     cv.put(Message.FLAGS, flags);
   4586                     context.getContentResolver().update(ContentUris.withAppendedId(
   4587                             Message.CONTENT_URI, originalMsgId), cv, null, null);
   4588                 }
   4589             }
   4590         }
   4591         return uiUri("uimessage", msg.mId);
   4592     }
   4593 
   4594     private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) {
   4595         final Mailbox mailbox =
   4596                 getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS);
   4597         if (mailbox == null) return null;
   4598         final Message msg;
   4599         if (extras.containsKey(BaseColumns._ID)) {
   4600             final long messageId = extras.getLong(BaseColumns._ID);
   4601             msg = Message.restoreMessageWithId(getContext(), messageId);
   4602         } else {
   4603             msg = new Message();
   4604         }
   4605         return uiSaveMessage(msg, mailbox, extras);
   4606     }
   4607 
   4608     private Uri uiSendDraftMessage(final long accountId, final Bundle extras) {
   4609         final Context context = getContext();
   4610         final Message msg;
   4611         if (extras.containsKey(BaseColumns._ID)) {
   4612             final long messageId = extras.getLong(BaseColumns._ID);
   4613             msg = Message.restoreMessageWithId(getContext(), messageId);
   4614         } else {
   4615             msg = new Message();
   4616         }
   4617 
   4618         if (msg == null) return null;
   4619         final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX);
   4620         if (mailbox == null) return null;
   4621         // Make sure the sent mailbox exists, since it will be necessary soon.
   4622         // TODO(yph): move system mailbox creation to somewhere sane.
   4623         final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT);
   4624         if (sentMailbox == null) return null;
   4625         final Uri messageUri = uiSaveMessage(msg, mailbox, extras);
   4626         // Kick observers
   4627         context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null);
   4628         return messageUri;
   4629     }
   4630 
   4631     private static void putIntegerLongOrBoolean(ContentValues values, String columnName,
   4632             Object value) {
   4633         if (value instanceof Integer) {
   4634             Integer intValue = (Integer)value;
   4635             values.put(columnName, intValue);
   4636         } else if (value instanceof Boolean) {
   4637             Boolean boolValue = (Boolean)value;
   4638             values.put(columnName, boolValue ? 1 : 0);
   4639         } else if (value instanceof Long) {
   4640             Long longValue = (Long)value;
   4641             values.put(columnName, longValue);
   4642         }
   4643     }
   4644 
   4645     /**
   4646      * Update the timestamps for the folders specified and notifies on the recent folder URI.
   4647      * @param folders array of folder Uris to update
   4648      * @return number of folders updated
   4649      */
   4650     private static int updateTimestamp(final Context context, String id, Uri[] folders){
   4651         int updated = 0;
   4652         final long now = System.currentTimeMillis();
   4653         final ContentResolver resolver = context.getContentResolver();
   4654         final ContentValues touchValues = new ContentValues();
   4655         for (final Uri folder : folders) {
   4656             touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now);
   4657             LogUtils.d(TAG, "updateStamp: %s updated", folder);
   4658             updated += resolver.update(folder, touchValues, null, null);
   4659         }
   4660         final Uri toNotify =
   4661                 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
   4662         LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify);
   4663         resolver.notifyChange(toNotify, null);
   4664         return updated;
   4665     }
   4666 
   4667     /**
   4668      * Updates the recent folders. The values to be updated are specified as ContentValues pairs
   4669      * of (Folder URI, access timestamp). Returns nonzero if successful, always.
   4670      * @param uri provider query uri
   4671      * @param values uri, timestamp pairs
   4672      * @return nonzero value always.
   4673      */
   4674     private int uiUpdateRecentFolders(Uri uri, ContentValues values) {
   4675         final int numFolders = values.size();
   4676         final String id = uri.getPathSegments().get(1);
   4677         final Uri[] folders = new Uri[numFolders];
   4678         final Context context = getContext();
   4679         int i = 0;
   4680         for (final String uriString : values.keySet()) {
   4681             folders[i] = Uri.parse(uriString);
   4682         }
   4683         return updateTimestamp(context, id, folders);
   4684     }
   4685 
   4686     /**
   4687      * Populates the recent folders according to the design.
   4688      * @param uri provider query uri
   4689      * @return the number of recent folders were populated.
   4690      */
   4691     private int uiPopulateRecentFolders(Uri uri) {
   4692         final Context context = getContext();
   4693         final String id = uri.getLastPathSegment();
   4694         final Uri[] recentFolders = defaultRecentFolders(id);
   4695         final int numFolders = recentFolders.length;
   4696         if (numFolders <= 0) {
   4697             return 0;
   4698         }
   4699         final int rowsUpdated = updateTimestamp(context, id, recentFolders);
   4700         LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated);
   4701         return rowsUpdated;
   4702     }
   4703 
   4704     private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
   4705         int result = 0;
   4706         Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
   4707         if (stateValue != null) {
   4708             // This is a command from UIProvider
   4709             long attachmentId = Long.parseLong(uri.getLastPathSegment());
   4710             Context context = getContext();
   4711             Attachment attachment =
   4712                     Attachment.restoreAttachmentWithId(context, attachmentId);
   4713             if (attachment == null) {
   4714                 // Went away; ah, well...
   4715                 return result;
   4716             }
   4717             int state = stateValue;
   4718             ContentValues values = new ContentValues();
   4719             if (state == UIProvider.AttachmentState.NOT_SAVED
   4720                     || state == UIProvider.AttachmentState.REDOWNLOADING) {
   4721                 // Set state, try to cancel request
   4722                 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED);
   4723                 values.put(AttachmentColumns.FLAGS,
   4724                         attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
   4725                 attachment.update(context, values);
   4726                 result = 1;
   4727             }
   4728             if (state == UIProvider.AttachmentState.DOWNLOADING
   4729                     || state == UIProvider.AttachmentState.REDOWNLOADING) {
   4730                 // Set state and destination; request download
   4731                 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING);
   4732                 Integer destinationValue =
   4733                         uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
   4734                 values.put(AttachmentColumns.UI_DESTINATION,
   4735                         destinationValue == null ? 0 : destinationValue);
   4736                 values.put(AttachmentColumns.FLAGS,
   4737                         attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
   4738 
   4739                 if (values.containsKey(AttachmentColumns.LOCATION) &&
   4740                         TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
   4741                     LogUtils.w(TAG, new Throwable(), "attachment with blank location");
   4742                 }
   4743 
   4744                 attachment.update(context, values);
   4745                 result = 1;
   4746             }
   4747             if (state == UIProvider.AttachmentState.SAVED) {
   4748                 // If this is an inline attachment, notify message has changed
   4749                 if (!TextUtils.isEmpty(attachment.mContentId)) {
   4750                     notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey);
   4751                 }
   4752                 result = 1;
   4753             }
   4754         }
   4755         return result;
   4756     }
   4757 
   4758     private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) {
   4759         // We need to mark seen separately
   4760         if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) {
   4761             final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN);
   4762 
   4763             if (seenValue == 1) {
   4764                 final String mailboxId = uri.getLastPathSegment();
   4765                 final int rows = markAllSeen(context, mailboxId);
   4766 
   4767                 if (uiValues.size() == 1) {
   4768                     // Nothing else to do, so return this value
   4769                     return rows;
   4770                 }
   4771             }
   4772         }
   4773 
   4774         final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true);
   4775         if (ourUri == null) return 0;
   4776         ContentValues ourValues = new ContentValues();
   4777         // This should only be called via update to "recent folders"
   4778         for (String columnName: uiValues.keySet()) {
   4779             if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) {
   4780                 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName));
   4781             }
   4782         }
   4783         return update(ourUri, ourValues, null, null);
   4784     }
   4785 
   4786     private int markAllSeen(final Context context, final String mailboxId) {
   4787         final SQLiteDatabase db = getDatabase(context);
   4788         final String table = Message.TABLE_NAME;
   4789         final ContentValues values = new ContentValues(1);
   4790         values.put(MessageColumns.FLAG_SEEN, 1);
   4791         final String whereClause = MessageColumns.MAILBOX_KEY + " = ?";
   4792         final String[] whereArgs = new String[] {mailboxId};
   4793 
   4794         return db.update(table, values, whereClause, whereArgs);
   4795     }
   4796 
   4797     private ContentValues convertUiMessageValues(Message message, ContentValues values) {
   4798         final ContentValues ourValues = new ContentValues();
   4799         for (String columnName : values.keySet()) {
   4800             final Object val = values.get(columnName);
   4801             if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
   4802                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
   4803             } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
   4804                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
   4805             } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) {
   4806                 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val);
   4807             } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
   4808                 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
   4809             } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) {
   4810                 // Skip this column, as the folders will also be specified  the RAW_FOLDERS column
   4811             } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) {
   4812                 // Convert from folder list uri to mailbox key
   4813                 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName));
   4814                 if (flist.folders.size() != 1) {
   4815                     LogUtils.e(TAG,
   4816                             "Incorrect number of folders for this message: Message is %s",
   4817                             message.mId);
   4818                 } else {
   4819                     final Folder f = flist.folders.get(0);
   4820                     final Uri uri = f.folderUri.fullUri;
   4821                     final Long mailboxId = Long.parseLong(uri.getLastPathSegment());
   4822                     putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
   4823                 }
   4824             } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) {
   4825                 Address[] fromList = Address.unpack(message.mFrom);
   4826                 final MailPrefs mailPrefs = MailPrefs.get(getContext());
   4827                 for (Address sender : fromList) {
   4828                     final String email = sender.getAddress();
   4829                     mailPrefs.setDisplayImagesFromSender(email, null);
   4830                 }
   4831             } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) ||
   4832                     columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) {
   4833                 // Ignore for now
   4834             } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) {
   4835                 // Email's conversation info is generated, not stored, so just ignore this update
   4836             } else {
   4837                 throw new IllegalArgumentException("Can't update " + columnName + " in message");
   4838             }
   4839         }
   4840         return ourValues;
   4841     }
   4842 
   4843     private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) {
   4844         final String idString = uri.getLastPathSegment();
   4845         try {
   4846             final long id = Long.parseLong(idString);
   4847             Uri ourUri = ContentUris.withAppendedId(newBaseUri, id);
   4848             if (asProvider) {
   4849                 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
   4850             }
   4851             return ourUri;
   4852         } catch (NumberFormatException e) {
   4853             return null;
   4854         }
   4855     }
   4856 
   4857     private Message getMessageFromLastSegment(Uri uri) {
   4858         long messageId = Long.parseLong(uri.getLastPathSegment());
   4859         return Message.restoreMessageWithId(getContext(), messageId);
   4860     }
   4861 
   4862     /**
   4863      * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
   4864      * clear out the undo list and start over
   4865      * @param uri the uri we're working on
   4866      * @param op the ContentProviderOperation to perform upon undo
   4867      */
   4868     private void addToSequence(Uri uri, ContentProviderOperation op) {
   4869         String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
   4870         if (sequenceString != null) {
   4871             int sequence = Integer.parseInt(sequenceString);
   4872             if (sequence > mLastSequence) {
   4873                 // Reset sequence
   4874                 mLastSequenceOps.clear();
   4875                 mLastSequence = sequence;
   4876             }
   4877             // TODO: Need something to indicate a change isn't ready (undoable)
   4878             mLastSequenceOps.add(op);
   4879         }
   4880     }
   4881 
   4882     // TODO: This should depend on flags on the mailbox...
   4883     private static boolean uploadsToServer(Context context, Mailbox m) {
   4884         if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
   4885                 m.mType == Mailbox.TYPE_SEARCH) {
   4886             return false;
   4887         }
   4888         String protocol = Account.getProtocol(context, m.mAccountKey);
   4889         EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   4890         return (info != null && info.syncChanges);
   4891     }
   4892 
   4893     private int uiUpdateMessage(Uri uri, ContentValues values) {
   4894         return uiUpdateMessage(uri, values, false);
   4895     }
   4896 
   4897     private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) {
   4898         Context context = getContext();
   4899         Message msg = getMessageFromLastSegment(uri);
   4900         if (msg == null) return 0;
   4901         Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
   4902         if (mailbox == null) return 0;
   4903         Uri ourBaseUri =
   4904                 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI :
   4905                     Message.CONTENT_URI;
   4906         Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true);
   4907         if (ourUri == null) return 0;
   4908 
   4909         // Special case - meeting response
   4910         if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) {
   4911             final EmailServiceProxy service =
   4912                     EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey);
   4913             try {
   4914                 service.sendMeetingResponse(msg.mId,
   4915                         values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN));
   4916                 // Delete the message immediately
   4917                 uiDeleteMessage(uri);
   4918                 Utility.showToast(context, R.string.confirm_response);
   4919                 // Notify box has changed so the deletion is reflected in the UI
   4920                 notifyUIConversationMailbox(mailbox.mId);
   4921             } catch (RemoteException e) {
   4922                 LogUtils.d(TAG, "Remote exception while sending meeting response");
   4923             }
   4924             return 1;
   4925         }
   4926 
   4927         // Another special case - deleting a draft.
   4928         final String operation = values.getAsString(
   4929                 UIProvider.ConversationOperations.OPERATION_KEY);
   4930         if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation)) {
   4931             uiDeleteMessage(uri);
   4932             return 1;
   4933         }
   4934 
   4935         ContentValues undoValues = new ContentValues();
   4936         ContentValues ourValues = convertUiMessageValues(msg, values);
   4937         for (String columnName: ourValues.keySet()) {
   4938             if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
   4939                 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
   4940             } else if (columnName.equals(MessageColumns.FLAG_READ)) {
   4941                 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
   4942             } else if (columnName.equals(MessageColumns.FLAG_SEEN)) {
   4943                 undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen);
   4944             } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
   4945                 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
   4946             }
   4947         }
   4948         if (undoValues.size() == 0) {
   4949             return -1;
   4950         }
   4951         final Boolean suppressUndo =
   4952                 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO);
   4953         if (suppressUndo == null || !suppressUndo) {
   4954             final ContentProviderOperation op =
   4955                     ContentProviderOperation.newUpdate(convertToEmailProviderUri(
   4956                             uri, ourBaseUri, false))
   4957                             .withValues(undoValues)
   4958                             .build();
   4959             addToSequence(uri, op);
   4960         }
   4961 
   4962         return update(ourUri, ourValues, null, null);
   4963     }
   4964 
   4965     /**
   4966      * Projection for use with getting mailbox & account keys for a message.
   4967      */
   4968     private static final String[] MESSAGE_KEYS_PROJECTION =
   4969             { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY };
   4970     private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0;
   4971     private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1;
   4972 
   4973     /**
   4974      * Notify necessary UI components in response to a message update.
   4975      * @param uri The {@link Uri} for this message update.
   4976      * @param messageId The id of the message that's been updated.
   4977      * @param values The {@link ContentValues} that were updated in the message.
   4978      */
   4979     private void handleMessageUpdateNotifications(final Uri uri, final String messageId,
   4980             final ContentValues values) {
   4981         if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
   4982             notifyUIConversation(uri);
   4983         }
   4984         notifyUIMessage(messageId);
   4985         // TODO: Ideally, also test that the values actually changed.
   4986         if (values.containsKey(MessageColumns.FLAG_READ) ||
   4987                 values.containsKey(MessageColumns.MAILBOX_KEY)) {
   4988             final Cursor c = query(
   4989                     Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(),
   4990                     MESSAGE_KEYS_PROJECTION, null, null, null);
   4991             if (c != null) {
   4992                 try {
   4993                     if (c.moveToFirst()) {
   4994                         notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN),
   4995                                 c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN));
   4996                     }
   4997                 } finally {
   4998                     c.close();
   4999                 }
   5000             }
   5001         }
   5002     }
   5003 
   5004     /**
   5005      * Perform a "Delete" operation
   5006      * @param uri message to delete
   5007      * @return number of rows affected
   5008      */
   5009     private int uiDeleteMessage(Uri uri) {
   5010         final Context context = getContext();
   5011         Message msg = getMessageFromLastSegment(uri);
   5012         if (msg == null) return 0;
   5013         Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
   5014         if (mailbox == null) return 0;
   5015         if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
   5016             // We actually delete these, including attachments
   5017             AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
   5018             final int r = context.getContentResolver().delete(
   5019                     ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null);
   5020             notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5021             notifyUIMessage(msg.mId);
   5022             return r;
   5023         }
   5024         Mailbox trashMailbox =
   5025                 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
   5026         if (trashMailbox == null) {
   5027             return 0;
   5028         }
   5029         ContentValues values = new ContentValues();
   5030         values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
   5031         final int r = uiUpdateMessage(uri, values, true);
   5032         notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5033         notifyUIMessage(msg.mId);
   5034         return r;
   5035     }
   5036 
   5037     public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
   5038     public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
   5039     // Currently unused
   5040     //public static final String PICKER_MESSAGE_ID = "picker_message_id";
   5041     public static final String PICKER_HEADER_ID = "picker_header_id";
   5042 
   5043     private int pickFolder(Uri uri, int type, int headerId) {
   5044         Context context = getContext();
   5045         Long acctId = Long.parseLong(uri.getLastPathSegment());
   5046         // For push imap, for example, we want the user to select the trash mailbox
   5047         Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION,
   5048                 null, null, null);
   5049         try {
   5050             if (ac.moveToFirst()) {
   5051                 final com.android.mail.providers.Account uiAccount =
   5052                         new com.android.mail.providers.Account(ac);
   5053                 Intent intent = new Intent(context, FolderPickerActivity.class);
   5054                 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
   5055                 intent.putExtra(PICKER_MAILBOX_TYPE, type);
   5056                 intent.putExtra(PICKER_HEADER_ID, headerId);
   5057                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   5058                 context.startActivity(intent);
   5059                 return 1;
   5060             }
   5061             return 0;
   5062         } finally {
   5063             ac.close();
   5064         }
   5065     }
   5066 
   5067     private int pickTrashFolder(Uri uri) {
   5068         return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
   5069     }
   5070 
   5071     private int pickSentFolder(Uri uri) {
   5072         return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
   5073     }
   5074 
   5075     private Cursor uiUndo(String[] projection) {
   5076         // First see if we have any operations saved
   5077         // TODO: Make sure seq matches
   5078         if (!mLastSequenceOps.isEmpty()) {
   5079             try {
   5080                 // TODO Always use this projection?  Or what's passed in?
   5081                 // Not sure if UI wants it, but I'm making a cursor of convo uri's
   5082                 MatrixCursor c = new MatrixCursorWithCachedColumns(
   5083                         new String[] {UIProvider.ConversationColumns.URI},
   5084                         mLastSequenceOps.size());
   5085                 for (ContentProviderOperation op: mLastSequenceOps) {
   5086                     c.addRow(new String[] {op.getUri().toString()});
   5087                 }
   5088                 // Just apply the batch and we're done!
   5089                 applyBatch(mLastSequenceOps);
   5090                 // But clear the operations
   5091                 mLastSequenceOps.clear();
   5092                 return c;
   5093             } catch (OperationApplicationException e) {
   5094                 LogUtils.d(TAG, "applyBatch exception");
   5095             }
   5096         }
   5097         return new MatrixCursorWithCachedColumns(projection, 0);
   5098     }
   5099 
   5100     private void notifyUIConversation(Uri uri) {
   5101         String id = uri.getLastPathSegment();
   5102         Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
   5103         if (msg != null) {
   5104             notifyUIConversationMailbox(msg.mMailboxKey);
   5105         }
   5106     }
   5107 
   5108     /**
   5109      * Notify about the Mailbox id passed in
   5110      * @param id the Mailbox id to be notified
   5111      */
   5112     private void notifyUIConversationMailbox(long id) {
   5113         notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
   5114         Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id);
   5115         if (mailbox == null) {
   5116             LogUtils.w(TAG, "No mailbox for notification: " + id);
   5117             return;
   5118         }
   5119         // Notify combined inbox...
   5120         if (mailbox.mType == Mailbox.TYPE_INBOX) {
   5121             notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER,
   5122                     EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX));
   5123         }
   5124         notifyWidgets(id);
   5125     }
   5126 
   5127     /**
   5128      * Notify about the message id passed in
   5129      * @param id the message id to be notified
   5130      */
   5131     private void notifyUIMessage(long id) {
   5132         notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
   5133     }
   5134 
   5135     /**
   5136      * Notify about the message id passed in
   5137      * @param id the message id to be notified
   5138      */
   5139     private void notifyUIMessage(String id) {
   5140         notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
   5141     }
   5142 
   5143     /**
   5144      * Notify about the Account id passed in
   5145      * @param id the Account id to be notified
   5146      */
   5147     private void notifyUIAccount(long id) {
   5148         // Notify on the specific account
   5149         notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id));
   5150 
   5151         // Notify on the all accounts list
   5152         notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
   5153     }
   5154 
   5155     // TODO: temporary workaround for ConversationCursor
   5156     @Deprecated
   5157     private static final int NOTIFY_FOLDER_LOOP_MESSAGE_ID = 0;
   5158     @Deprecated
   5159     private Handler mFolderNotifierHandler;
   5160 
   5161     /**
   5162      * Notify about a folder update. Because folder changes can affect the conversation cursor's
   5163      * extras, the conversation must also be notified here.
   5164      * @param folderId the folder id to be notified
   5165      * @param accountId the account id to be notified (for folder list notification).
   5166      */
   5167     private void notifyUIFolder(final String folderId, final long accountId) {
   5168         notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
   5169         notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId);
   5170         if (accountId != Account.NO_ACCOUNT) {
   5171             notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
   5172         }
   5173 
   5174         // Notify for combined account too
   5175         // TODO: might be nice to only notify when an inbox changes
   5176         notifyUI(UIPROVIDER_FOLDER_NOTIFIER,
   5177                 getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX));
   5178         notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID);
   5179 
   5180         // TODO: temporary workaround for ConversationCursor
   5181         synchronized (this) {
   5182             if (mFolderNotifierHandler == null) {
   5183                 mFolderNotifierHandler = new Handler(Looper.getMainLooper(),
   5184                         new Callback() {
   5185                             @Override
   5186                             public boolean handleMessage(final android.os.Message message) {
   5187                                 final String folderId = (String) message.obj;
   5188                                 LogUtils.d(TAG, "Notifying conversation Uri %s twice", folderId);
   5189                                 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
   5190                                 return true;
   5191                             }
   5192                         });
   5193             }
   5194         }
   5195         mFolderNotifierHandler.removeMessages(NOTIFY_FOLDER_LOOP_MESSAGE_ID);
   5196         android.os.Message message = android.os.Message.obtain(mFolderNotifierHandler,
   5197                 NOTIFY_FOLDER_LOOP_MESSAGE_ID);
   5198         message.obj = folderId;
   5199         mFolderNotifierHandler.sendMessageDelayed(message, 2000);
   5200     }
   5201 
   5202     private void notifyUIFolder(final long folderId, final long accountId) {
   5203         notifyUIFolder(Long.toString(folderId), accountId);
   5204     }
   5205 
   5206     private void notifyUI(Uri uri, String id) {
   5207         final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri;
   5208         getContext().getContentResolver().notifyChange(notifyUri, null);
   5209     }
   5210 
   5211     private void notifyUI(Uri uri, long id) {
   5212         notifyUI(uri, Long.toString(id));
   5213     }
   5214 
   5215     private Mailbox getMailbox(final Uri uri) {
   5216         final long id = Long.parseLong(uri.getLastPathSegment());
   5217         return Mailbox.restoreMailboxWithId(getContext(), id);
   5218     }
   5219 
   5220     /**
   5221      * Create an android.accounts.Account object for this account.
   5222      * @param accountId id of account to load.
   5223      * @return an android.accounts.Account for this account, or null if we can't load it.
   5224      */
   5225     private android.accounts.Account getAccountManagerAccount(final long accountId) {
   5226         final Context context = getContext();
   5227         final Account account = Account.restoreAccountWithId(context, accountId);
   5228         if (account == null) return null;
   5229         return getAccountManagerAccount(context, account.mEmailAddress,
   5230                 account.getProtocol(context));
   5231     }
   5232 
   5233     /**
   5234      * Create an android.accounts.Account object for an emailAddress/protocol pair.
   5235      * @param context A {@link Context}.
   5236      * @param emailAddress The email address we're interested in.
   5237      * @param protocol The protocol we're intereted in.
   5238      * @return an {@link android.accounts.Account} for this info.
   5239      */
   5240     private static android.accounts.Account getAccountManagerAccount(final Context context,
   5241             final String emailAddress, final String protocol) {
   5242         final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
   5243         return new android.accounts.Account(emailAddress, info.accountType);
   5244     }
   5245 
   5246     /**
   5247      * Update an account's periodic sync if the sync interval has changed.
   5248      * @param accountId id for the account to update.
   5249      * @param values the ContentValues for this update to the account.
   5250      */
   5251     private void updateAccountSyncInterval(final long accountId, final ContentValues values) {
   5252         final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL);
   5253         if (syncInterval == null) {
   5254             // No change to the sync interval.
   5255             return;
   5256         }
   5257         final android.accounts.Account account = getAccountManagerAccount(accountId);
   5258         if (account == null) {
   5259             // Unable to load the account, or unknown protocol.
   5260             return;
   5261         }
   5262 
   5263         LogUtils.d(TAG, "Setting sync interval for account " + accountId + " to " + syncInterval +
   5264                 " minutes");
   5265 
   5266         // First remove all existing periodic syncs.
   5267         final List<PeriodicSync> syncs =
   5268                 ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY);
   5269         for (final PeriodicSync sync : syncs) {
   5270             ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras);
   5271         }
   5272 
   5273         // Only positive values of sync interval indicate periodic syncs. The value is in minutes,
   5274         // while addPeriodicSync expects its time in seconds.
   5275         if (syncInterval > 0) {
   5276             ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY,
   5277                     syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
   5278         }
   5279     }
   5280 
   5281     /**
   5282      * Request a sync.
   5283      * @param account The {@link android.accounts.Account} we want to sync.
   5284      * @param mailboxId The mailbox id we want to sync (or one of the special constants in
   5285      *                  {@link Mailbox}).
   5286      * @param deltaMessageCount If we're requesting a load more, the number of additional messages
   5287      *                          to sync.
   5288      */
   5289     private static void startSync(final android.accounts.Account account, final long mailboxId,
   5290             final int deltaMessageCount) {
   5291         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
   5292         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   5293         extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   5294         extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   5295         if (deltaMessageCount != 0) {
   5296             extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
   5297         }
   5298         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
   5299                 EmailContent.CONTENT_URI.toString());
   5300         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
   5301                 SYNC_STATUS_CALLBACK_METHOD);
   5302         ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
   5303         LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
   5304                 extras.toString());
   5305     }
   5306 
   5307     /**
   5308      * Request a sync.
   5309      * @param mailbox The {@link Mailbox} we want to sync.
   5310      * @param deltaMessageCount If we're requesting a load more, the number of additional messages
   5311      *                          to sync.
   5312      */
   5313     private void startSync(final Mailbox mailbox, final int deltaMessageCount) {
   5314         final android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey);
   5315         startSync(account, mailbox.mId, deltaMessageCount);
   5316     }
   5317 
   5318     /**
   5319      * Restart any push operations for an account.
   5320      * @param account The {@link android.accounts.Account} we're interested in.
   5321      */
   5322     private static void restartPush(final android.accounts.Account account) {
   5323         final Bundle extras = new Bundle();
   5324         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
   5325         extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
   5326         extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
   5327         extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
   5328         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
   5329                 EmailContent.CONTENT_URI.toString());
   5330         extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
   5331                 SYNC_STATUS_CALLBACK_METHOD);
   5332         ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
   5333         LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
   5334                 extras.toString());
   5335     }
   5336 
   5337     private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) {
   5338         if (mailbox != null) {
   5339             RefreshStatusMonitor.getInstance(getContext())
   5340                     .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() {
   5341                 @Override
   5342                 public void onRefreshCompleted(long mailboxId, int result) {
   5343                     final ContentValues values = new ContentValues();
   5344                     values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
   5345                     values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
   5346                     mDatabase.update(
   5347                             Mailbox.TABLE_NAME,
   5348                             values,
   5349                             WHERE_ID,
   5350                             new String[] { String.valueOf(mailboxId) });
   5351                     notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
   5352                 }
   5353 
   5354                 @Override
   5355                 public void onTimeout(long mailboxId) {
   5356                     // todo
   5357                 }
   5358             });
   5359             startSync(mailbox, deltaMessageCount);
   5360         }
   5361         return null;
   5362     }
   5363 
   5364     //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
   5365     public static final int VISIBLE_LIMIT_INCREMENT = 10;
   5366     //Number of additional messages to load when a user selects "Load more..." in a search
   5367     public static final int SEARCH_MORE_INCREMENT = 10;
   5368 
   5369     private Cursor uiFolderLoadMore(final Mailbox mailbox) {
   5370         if (mailbox == null) return null;
   5371         if (mailbox.mType == Mailbox.TYPE_SEARCH) {
   5372             // Ask for 10 more messages
   5373             mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
   5374             runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId);
   5375         } else {
   5376             uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT);
   5377         }
   5378         return null;
   5379     }
   5380 
   5381     private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
   5382     private SearchParams mSearchParams;
   5383 
   5384     /**
   5385      * Returns the search mailbox for the specified account, creating one if necessary
   5386      * @return the search mailbox for the passed in account
   5387      */
   5388     private Mailbox getSearchMailbox(long accountId) {
   5389         Context context = getContext();
   5390         Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
   5391         if (m == null) {
   5392             m = new Mailbox();
   5393             m.mAccountKey = accountId;
   5394             m.mServerId = SEARCH_MAILBOX_SERVER_ID;
   5395             m.mFlagVisible = false;
   5396             m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
   5397             m.mSyncInterval = 0;
   5398             m.mType = Mailbox.TYPE_SEARCH;
   5399             m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
   5400             m.mParentKey = Mailbox.NO_MAILBOX;
   5401             m.save(context);
   5402         }
   5403         return m;
   5404     }
   5405 
   5406     private void runSearchQuery(final Context context, final long accountId,
   5407             final long searchMailboxId) {
   5408         LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d",
   5409                 accountId, searchMailboxId);
   5410 
   5411         // Start the search running in the background
   5412         new AsyncTask<Void, Void, Void>() {
   5413             @Override
   5414             public Void doInBackground(Void... params) {
   5415                 final EmailServiceProxy service =
   5416                         EmailServiceUtils.getServiceForAccount(context, accountId);
   5417                 if (service != null) {
   5418                     try {
   5419                         final int totalCount =
   5420                                 service.searchMessages(accountId, mSearchParams, searchMailboxId);
   5421 
   5422                         // Save away the total count
   5423                         final ContentValues cv = new ContentValues(1);
   5424                         cv.put(MailboxColumns.TOTAL_COUNT, totalCount);
   5425                         update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), cv,
   5426                                 null, null);
   5427                         LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d",
   5428                                 totalCount);
   5429                     } catch (RemoteException e) {
   5430                         LogUtils.e("searchMessages", "RemoteException", e);
   5431                     }
   5432                 }
   5433                 return null;
   5434             }
   5435         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
   5436     }
   5437 
   5438     // This handles an initial search query. More results are loaded using uiFolderLoadMore.
   5439     private Cursor uiSearch(Uri uri, String[] projection) {
   5440         LogUtils.d(TAG, "runSearchQuery in search %s", uri);
   5441         final long accountId = Long.parseLong(uri.getLastPathSegment());
   5442 
   5443         // TODO: Check the actual mailbox
   5444         Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
   5445         if (inbox == null) {
   5446             LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account "
   5447                     + accountId);
   5448 
   5449             return null;
   5450         }
   5451 
   5452         String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
   5453         if (filter == null) {
   5454             throw new IllegalArgumentException("No query parameter in search query");
   5455         }
   5456 
   5457         // Find/create our search mailbox
   5458         Mailbox searchMailbox = getSearchMailbox(accountId);
   5459         final long searchMailboxId = searchMailbox.mId;
   5460 
   5461         mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
   5462 
   5463         final Context context = getContext();
   5464         if (mSearchParams.mOffset == 0) {
   5465             // TODO: This conditional is unnecessary, just two lines earlier we created
   5466             // mSearchParams using a constructor that never sets mOffset.
   5467             LogUtils.d(TAG, "deleting existing search results.");
   5468 
   5469             // Delete existing contents of search mailbox
   5470             ContentResolver resolver = context.getContentResolver();
   5471             resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
   5472                     null);
   5473             final ContentValues cv = new ContentValues(1);
   5474             // For now, use the actual query as the name of the mailbox
   5475             cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
   5476             resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
   5477                     cv, null, null);
   5478         }
   5479 
   5480         // Start the search running in the background
   5481         runSearchQuery(context, accountId, searchMailboxId);
   5482 
   5483         // This will look just like a "normal" folder
   5484         return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
   5485                 searchMailbox.mId), projection, false);
   5486     }
   5487 
   5488     private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
   5489 
   5490     /**
   5491      * Delete an account and clean it up
   5492      */
   5493     private int uiDeleteAccount(Uri uri) {
   5494         Context context = getContext();
   5495         long accountId = Long.parseLong(uri.getLastPathSegment());
   5496         try {
   5497             // Get the account URI.
   5498             final Account account = Account.restoreAccountWithId(context, accountId);
   5499             if (account == null) {
   5500                 return 0; // Already deleted?
   5501             }
   5502 
   5503             deleteAccountData(context, accountId);
   5504 
   5505             // Now delete the account itself
   5506             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
   5507             context.getContentResolver().delete(uri, null, null);
   5508 
   5509             // Clean up
   5510             AccountBackupRestore.backup(context);
   5511             SecurityPolicy.getInstance(context).reducePolicies();
   5512             MailActivityEmail.setServicesEnabledSync(context);
   5513             return 1;
   5514         } catch (Exception e) {
   5515             LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e);
   5516         }
   5517         return 0;
   5518     }
   5519 
   5520     private int uiDeleteAccountData(Uri uri) {
   5521         Context context = getContext();
   5522         long accountId = Long.parseLong(uri.getLastPathSegment());
   5523         // Get the account URI.
   5524         final Account account = Account.restoreAccountWithId(context, accountId);
   5525         if (account == null) {
   5526             return 0; // Already deleted?
   5527         }
   5528         deleteAccountData(context, accountId);
   5529         return 1;
   5530     }
   5531 
   5532     /** Projection used for getting email address for an account. */
   5533     private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
   5534 
   5535     private static void deleteAccountData(Context context, long accountId) {
   5536         // We will delete PIM data, but by the time the asynchronous call to do that happens,
   5537         // the account may have been deleted from the DB. Therefore we have to get the email
   5538         // address now and send that, rather than the account id.
   5539         final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI,
   5540                 ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
   5541                 new String[] {Long.toString(accountId)}, null, 0);
   5542         if (emailAddress == null) {
   5543             LogUtils.e(TAG, "Could not find email address for account %d", accountId);
   5544         }
   5545 
   5546         // Delete synced attachments
   5547         AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
   5548 
   5549         // Delete all mailboxes.
   5550         ContentResolver resolver = context.getContentResolver();
   5551         String[] accountIdArgs = new String[] { Long.toString(accountId) };
   5552         resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
   5553 
   5554         // Delete account sync key.
   5555         final ContentValues cv = new ContentValues();
   5556         cv.putNull(Account.SYNC_KEY);
   5557         resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
   5558 
   5559         // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
   5560         if (emailAddress != null) {
   5561             final IEmailService service =
   5562                     EmailServiceUtils.getServiceForAccount(context, accountId);
   5563             if (service != null) {
   5564                 try {
   5565                     service.deleteAccountPIMData(emailAddress);
   5566                 } catch (final RemoteException e) {
   5567                     // Can't do anything about this
   5568                 }
   5569             }
   5570         }
   5571     }
   5572 
   5573     private int[] mSavedWidgetIds = new int[0];
   5574     private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>();
   5575     private AppWidgetManager mAppWidgetManager;
   5576     private ComponentName mEmailComponent;
   5577 
   5578     private void notifyWidgets(long mailboxId) {
   5579         Context context = getContext();
   5580         // Lazily initialize these
   5581         if (mAppWidgetManager == null) {
   5582             mAppWidgetManager = AppWidgetManager.getInstance(context);
   5583             mEmailComponent = new ComponentName(context, WidgetProvider.getProviderName(context));
   5584         }
   5585 
   5586         // See if we have to populate our array of mailboxes used in widgets
   5587         int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent);
   5588         if (!Arrays.equals(widgetIds, mSavedWidgetIds)) {
   5589             mSavedWidgetIds = widgetIds;
   5590             String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds);
   5591             // widgetInfo now has pairs of account uri/folder uri
   5592             mWidgetNotifyMailboxes.clear();
   5593             for (String[] widgetInfo: widgetInfos) {
   5594                 try {
   5595                     if (widgetInfo == null || TextUtils.isEmpty(widgetInfo[1])) continue;
   5596                     long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment());
   5597                     if (!isCombinedMailbox(id)) {
   5598                         // For a regular mailbox, just add it to the list
   5599                         if (!mWidgetNotifyMailboxes.contains(id)) {
   5600                             mWidgetNotifyMailboxes.add(id);
   5601                         }
   5602                     } else {
   5603                         switch (getVirtualMailboxType(id)) {
   5604                             // We only handle the combined inbox in widgets
   5605                             case Mailbox.TYPE_INBOX:
   5606                                 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
   5607                                         MailboxColumns.TYPE + "=?",
   5608                                         new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null);
   5609                                 try {
   5610                                     while (c.moveToNext()) {
   5611                                         mWidgetNotifyMailboxes.add(
   5612                                                 c.getLong(Mailbox.ID_PROJECTION_COLUMN));
   5613                                     }
   5614                                 } finally {
   5615                                     c.close();
   5616                                 }
   5617                                 break;
   5618                         }
   5619                     }
   5620                 } catch (NumberFormatException e) {
   5621                     // Move along
   5622                 }
   5623             }
   5624         }
   5625 
   5626         // If our mailbox needs to be notified, do so...
   5627         if (mWidgetNotifyMailboxes.contains(mailboxId)) {
   5628             Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED);
   5629             intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId));
   5630             intent.setType(EMAIL_APP_MIME_TYPE);
   5631             context.sendBroadcast(intent);
   5632          }
   5633     }
   5634 
   5635     @Override
   5636     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
   5637         Context context = getContext();
   5638         writer.println("Installed services:");
   5639         for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) {
   5640             writer.println("  " + info);
   5641         }
   5642         writer.println();
   5643         writer.println("Accounts: ");
   5644         Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
   5645         if (cursor.getCount() == 0) {
   5646             writer.println("  None");
   5647         }
   5648         try {
   5649             while (cursor.moveToNext()) {
   5650                 Account account = new Account();
   5651                 account.restore(cursor);
   5652                 writer.println("  Account " + account.mDisplayName);
   5653                 HostAuth hostAuth =
   5654                         HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
   5655                 if (hostAuth != null) {
   5656                     writer.println("    Protocol = " + hostAuth.mProtocol +
   5657                             (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " +
   5658                                     account.mProtocolVersion));
   5659                 }
   5660             }
   5661         } finally {
   5662             cursor.close();
   5663         }
   5664     }
   5665 
   5666     synchronized public Handler getDelayedSyncHandler() {
   5667         if (mDelayedSyncHandler == null) {
   5668             mDelayedSyncHandler = new Handler(getContext().getMainLooper(), new Callback() {
   5669                 @Override
   5670                 public boolean handleMessage(android.os.Message msg) {
   5671                     synchronized (mDelayedSyncRequests) {
   5672                         final SyncRequestMessage request = (SyncRequestMessage) msg.obj;
   5673                         // TODO: It's possible that the account is deleted by the time we get here
   5674                         // It would be nice if we could validate it before trying to sync
   5675                         final android.accounts.Account account = request.mAccount;
   5676                         final Bundle extras = Mailbox.createSyncBundle(request.mMailboxId);
   5677                         ContentResolver.requestSync(account, request.mAuthority, extras);
   5678                         LogUtils.i(TAG, "requestSync getDelayedSyncHandler %s, %s",
   5679                                 account.toString(), extras.toString());
   5680                         mDelayedSyncRequests.remove(request);
   5681                         return true;
   5682                     }
   5683                 }
   5684             });
   5685         }
   5686         return mDelayedSyncHandler;
   5687     }
   5688 
   5689     private class SyncRequestMessage {
   5690         private final String mAuthority;
   5691         private final android.accounts.Account mAccount;
   5692         private final long mMailboxId;
   5693 
   5694         private SyncRequestMessage(final String authority, final android.accounts.Account account,
   5695                 final long mailboxId) {
   5696             mAuthority = authority;
   5697             mAccount = account;
   5698             mMailboxId = mailboxId;
   5699         }
   5700 
   5701         @Override
   5702         public boolean equals(Object o) {
   5703             if (this == o) {
   5704                 return true;
   5705             }
   5706             if (o == null || getClass() != o.getClass()) {
   5707                 return false;
   5708             }
   5709 
   5710             SyncRequestMessage that = (SyncRequestMessage) o;
   5711 
   5712             return mAccount.equals(that.mAccount)
   5713                     && mMailboxId == that.mMailboxId
   5714                     && mAuthority.equals(that.mAuthority);
   5715         }
   5716 
   5717         @Override
   5718         public int hashCode() {
   5719             int result = mAuthority.hashCode();
   5720             result = 31 * result + mAccount.hashCode();
   5721             result = 31 * result + (int) (mMailboxId ^ (mMailboxId >>> 32));
   5722             return result;
   5723         }
   5724     }
   5725 }
   5726