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