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