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