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