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