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