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