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