EmailProvider.java revision 6550ae97731282e2427c3130c4ea316dfce5538b
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.content.ContentProvider;
20import android.content.ContentProviderOperation;
21import android.content.ContentProviderResult;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.OperationApplicationException;
28import android.content.UriMatcher;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.database.MatrixCursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.database.sqlite.SQLiteException;
34import android.net.Uri;
35import android.os.RemoteException;
36import android.provider.BaseColumns;
37import android.text.TextUtils;
38import android.util.Log;
39
40import com.android.common.content.ProjectionMap;
41import com.android.email.Email;
42import com.android.email.Preferences;
43import com.android.email.R;
44import com.android.email.SecurityPolicy;
45import com.android.email.provider.ContentCache.CacheToken;
46import com.android.email.service.AttachmentDownloadService;
47import com.android.email.service.EmailServiceUtils;
48import com.android.emailcommon.Logging;
49import com.android.emailcommon.provider.Account;
50import com.android.emailcommon.provider.EmailContent;
51import com.android.emailcommon.provider.EmailContent.AccountColumns;
52import com.android.emailcommon.provider.EmailContent.Attachment;
53import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
54import com.android.emailcommon.provider.EmailContent.Body;
55import com.android.emailcommon.provider.EmailContent.BodyColumns;
56import com.android.emailcommon.provider.EmailContent.MailboxColumns;
57import com.android.emailcommon.provider.EmailContent.Message;
58import com.android.emailcommon.provider.EmailContent.MessageColumns;
59import com.android.emailcommon.provider.EmailContent.PolicyColumns;
60import com.android.emailcommon.provider.EmailContent.SyncColumns;
61import com.android.emailcommon.provider.HostAuth;
62import com.android.emailcommon.provider.Mailbox;
63import com.android.emailcommon.provider.Policy;
64import com.android.emailcommon.provider.QuickResponse;
65import com.android.emailcommon.service.EmailServiceProxy;
66import com.android.emailcommon.service.IEmailService;
67import com.android.emailcommon.service.IEmailServiceCallback;
68import com.android.emailcommon.service.SearchParams;
69import com.android.emailcommon.utility.AttachmentUtilities;
70import com.android.mail.providers.UIProvider;
71import com.android.mail.providers.UIProvider.AccountCapabilities;
72import com.android.mail.providers.UIProvider.ConversationPriority;
73import com.android.mail.providers.UIProvider.ConversationSendingState;
74import com.google.common.annotations.VisibleForTesting;
75
76import java.io.File;
77import java.util.ArrayList;
78import java.util.Arrays;
79import java.util.Collection;
80import java.util.HashMap;
81import java.util.List;
82import java.util.Map;
83
84public class EmailProvider extends ContentProvider {
85
86    private static final String TAG = "EmailProvider";
87
88    protected static final String DATABASE_NAME = "EmailProvider.db";
89    protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
90    protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db";
91
92    public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED";
93    public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS =
94        "com.android.email.ATTACHMENT_UPDATED_FLAGS";
95
96    /**
97     * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this
98     * {@link android.content.Intent} and update accordingly. However, this can be very broad and
99     * is NOT the preferred way of getting notification.
100     */
101    public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED =
102        "com.android.email.MESSAGE_LIST_DATASET_CHANGED";
103
104    public static final String EMAIL_MESSAGE_MIME_TYPE =
105        "vnd.android.cursor.item/email-message";
106    public static final String EMAIL_ATTACHMENT_MIME_TYPE =
107        "vnd.android.cursor.item/email-attachment";
108
109    public static final Uri INTEGRITY_CHECK_URI =
110        Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck");
111    public static final Uri ACCOUNT_BACKUP_URI =
112        Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup");
113    public static final Uri FOLDER_STATUS_URI =
114            Uri.parse("content://" + EmailContent.AUTHORITY + "/status");
115    public static final Uri FOLDER_REFRESH_URI =
116            Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh");
117
118    /** Appended to the notification URI for delete operations */
119    public static final String NOTIFICATION_OP_DELETE = "delete";
120    /** Appended to the notification URI for insert operations */
121    public static final String NOTIFICATION_OP_INSERT = "insert";
122    /** Appended to the notification URI for update operations */
123    public static final String NOTIFICATION_OP_UPDATE = "update";
124
125    // Definitions for our queries looking for orphaned messages
126    private static final String[] ORPHANS_PROJECTION
127        = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY};
128    private static final int ORPHANS_ID = 0;
129    private static final int ORPHANS_MAILBOX_KEY = 1;
130
131    private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
132
133    // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all
134    // critical mailboxes, host auth's, accounts, and policies are cached
135    private static final int MAX_CACHED_ACCOUNTS = 16;
136    // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible)
137    private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6;
138
139    // We'll cache the following four tables; sizes are best estimates of effective values
140    private final ContentCache mCacheAccount =
141        new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS);
142    private final ContentCache mCacheHostAuth =
143        new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2);
144    /*package*/ final ContentCache mCacheMailbox =
145        new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION,
146                MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2));
147    private final ContentCache mCacheMessage =
148        new ContentCache("Message", Message.CONTENT_PROJECTION, 8);
149    private final ContentCache mCachePolicy =
150        new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS);
151
152    private static final int ACCOUNT_BASE = 0;
153    private static final int ACCOUNT = ACCOUNT_BASE;
154    private static final int ACCOUNT_ID = ACCOUNT_BASE + 1;
155    private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2;
156    private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3;
157    private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4;
158    private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5;
159
160    private static final int MAILBOX_BASE = 0x1000;
161    private static final int MAILBOX = MAILBOX_BASE;
162    private static final int MAILBOX_ID = MAILBOX_BASE + 1;
163    private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2;
164    private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3;
165    private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 4;
166    private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 5;
167
168    private static final int MESSAGE_BASE = 0x2000;
169    private static final int MESSAGE = MESSAGE_BASE;
170    private static final int MESSAGE_ID = MESSAGE_BASE + 1;
171    private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
172
173    private static final int ATTACHMENT_BASE = 0x3000;
174    private static final int ATTACHMENT = ATTACHMENT_BASE;
175    private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1;
176    private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2;
177
178    private static final int HOSTAUTH_BASE = 0x4000;
179    private static final int HOSTAUTH = HOSTAUTH_BASE;
180    private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
181
182    private static final int UPDATED_MESSAGE_BASE = 0x5000;
183    private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
184    private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
185
186    private static final int DELETED_MESSAGE_BASE = 0x6000;
187    private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
188    private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
189
190    private static final int POLICY_BASE = 0x7000;
191    private static final int POLICY = POLICY_BASE;
192    private static final int POLICY_ID = POLICY_BASE + 1;
193
194    private static final int QUICK_RESPONSE_BASE = 0x8000;
195    private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE;
196    private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1;
197    private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2;
198
199    private static final int UI_BASE = 0x9000;
200    private static final int UI_FOLDERS = UI_BASE;
201    private static final int UI_SUBFOLDERS = UI_BASE + 1;
202    private static final int UI_MESSAGES = UI_BASE + 2;
203    private static final int UI_MESSAGE = UI_BASE + 3;
204    private static final int UI_SENDMAIL = UI_BASE + 4;
205    private static final int UI_UNDO = UI_BASE + 5;
206    private static final int UI_SAVEDRAFT = UI_BASE + 6;
207    private static final int UI_UPDATEDRAFT = UI_BASE + 7;
208    private static final int UI_SENDDRAFT = UI_BASE + 8;
209    private static final int UI_FOLDER_REFRESH = UI_BASE + 9;
210    private static final int UI_FOLDER = UI_BASE + 10;
211    private static final int UI_ACCOUNT = UI_BASE + 11;
212    private static final int UI_ACCTS = UI_BASE + 12;
213    private static final int UI_SETTINGS = UI_BASE + 13;
214    private static final int UI_ATTACHMENTS = UI_BASE + 14;
215    private static final int UI_ATTACHMENT = UI_BASE + 15;
216    private static final int UI_SEARCH = UI_BASE + 16;
217    private static final int UI_ACCOUNT_DATA = UI_BASE + 17;
218    private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 18;
219
220    // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS
221    private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE;
222
223    // DO NOT CHANGE BODY_BASE!!
224    private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000;
225    private static final int BODY = BODY_BASE;
226    private static final int BODY_ID = BODY_BASE + 1;
227
228    private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
229
230    // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000,
231    // MESSAGE_BASE = 0x1000, etc.)
232    private static final String[] TABLE_NAMES = {
233        Account.TABLE_NAME,
234        Mailbox.TABLE_NAME,
235        Message.TABLE_NAME,
236        Attachment.TABLE_NAME,
237        HostAuth.TABLE_NAME,
238        Message.UPDATED_TABLE_NAME,
239        Message.DELETED_TABLE_NAME,
240        Policy.TABLE_NAME,
241        QuickResponse.TABLE_NAME,
242        null,  // UI
243        Body.TABLE_NAME,
244    };
245
246    // CONTENT_CACHES MUST remain in the order of the BASE constants above
247    private final ContentCache[] mContentCaches = {
248        mCacheAccount,
249        mCacheMailbox,
250        mCacheMessage,
251        null, // Attachment
252        mCacheHostAuth,
253        null, // Updated message
254        null, // Deleted message
255        mCachePolicy,
256        null, // Quick response
257        null, // Body
258        null  // UI
259    };
260
261    // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above
262    private static final String[][] CACHE_PROJECTIONS = {
263        Account.CONTENT_PROJECTION,
264        Mailbox.CONTENT_PROJECTION,
265        Message.CONTENT_PROJECTION,
266        null, // Attachment
267        HostAuth.CONTENT_PROJECTION,
268        null, // Updated message
269        null, // Deleted message
270        Policy.CONTENT_PROJECTION,
271        null,  // Quick response
272        null,  // Body
273        null   // UI
274    };
275
276    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
277
278    private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" +
279        Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," +
280        Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")";
281
282    /**
283     * Let's only generate these SQL strings once, as they are used frequently
284     * Note that this isn't relevant for table creation strings, since they are used only once
285     */
286    private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
287        Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
288        EmailContent.RECORD_ID + '=';
289
290    private static final String UPDATED_MESSAGE_DELETE = "delete from " +
291        Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '=';
292
293    private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
294        Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
295        EmailContent.RECORD_ID + '=';
296
297    private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
298        " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY +
299        " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " +
300        Message.TABLE_NAME + ')';
301
302    private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
303        " where " + BodyColumns.MESSAGE_KEY + '=';
304
305    private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?";
306
307    private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
308    private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
309
310    public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId";
311
312    // For undo handling
313    private int mLastSequence = -1;
314    private ArrayList<ContentProviderOperation> mLastSequenceOps =
315            new ArrayList<ContentProviderOperation>();
316
317    // Query parameter indicating the command came from UIProvider
318    private static final String IS_UIPROVIDER = "is_uiprovider";
319
320    static {
321        // Email URI matching table
322        UriMatcher matcher = sURIMatcher;
323
324        // All accounts
325        matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT);
326        // A specific account
327        // insert into this URI causes a mailbox to be added to the account
328        matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID);
329        matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID);
330
331        // Special URI to reset the new message count.  Only update works, and content values
332        // will be ignored.
333        matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount",
334                ACCOUNT_RESET_NEW_COUNT);
335        matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#",
336                ACCOUNT_RESET_NEW_COUNT_ID);
337
338        // All mailboxes
339        matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX);
340        // A specific mailbox
341        // insert into this URI causes a message to be added to the mailbox
342        // ** NOTE For now, the accountKey must be set manually in the values!
343        matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID);
344        matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#",
345                MAILBOX_ID_FROM_ACCOUNT_AND_TYPE);
346        matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", MAILBOX_NOTIFICATION);
347        matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#",
348                MAILBOX_MOST_RECENT_MESSAGE);
349
350        // All messages
351        matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE);
352        // A specific message
353        // insert into this URI causes an attachment to be added to the message
354        matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID);
355
356        // A specific attachment
357        matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT);
358        // A specific attachment (the header information)
359        matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID);
360        // The attachments of a specific message (query only) (insert & delete TBD)
361        matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#",
362                ATTACHMENTS_MESSAGE_ID);
363
364        // All mail bodies
365        matcher.addURI(EmailContent.AUTHORITY, "body", BODY);
366        // A specific mail body
367        matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
368
369        // All hostauth records
370        matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
371        // A specific hostauth
372        matcher.addURI(EmailContent.AUTHORITY, "hostauth/#", HOSTAUTH_ID);
373
374        // Atomically a constant value to a particular field of a mailbox/account
375        matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#",
376                MAILBOX_ID_ADD_TO_FIELD);
377        matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#",
378                ACCOUNT_ID_ADD_TO_FIELD);
379
380        /**
381         * THIS URI HAS SPECIAL SEMANTICS
382         * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK
383         * TO A SERVER VIA A SYNC ADAPTER
384         */
385        matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
386
387        /**
388         * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
389         * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
390         * BY THE UI APPLICATION
391         */
392        // All deleted messages
393        matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE);
394        // A specific deleted message
395        matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
396
397        // All updated messages
398        matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
399        // A specific updated message
400        matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
401
402        CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues();
403        CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0);
404
405        matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY);
406        matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID);
407
408        // All quick responses
409        matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE);
410        // A specific quick response
411        matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID);
412        // All quick responses associated with a particular account id
413        matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#",
414                QUICK_RESPONSE_ACCOUNT_ID);
415
416        matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS);
417        matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS);
418        matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES);
419        matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE);
420        matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL);
421        matcher.addURI(EmailContent.AUTHORITY, "uiundo/#", UI_UNDO);
422        matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT);
423        matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT);
424        matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT);
425        matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH);
426        matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER);
427        matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT);
428        matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS);
429        matcher.addURI(EmailContent.AUTHORITY, "uisettings/#", UI_SETTINGS);
430        matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS);
431        matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT);
432        matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
433        matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
434        matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
435    }
436
437    /**
438     * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
439     * @param uri the Uri to match
440     * @return the match value
441     */
442    private static int findMatch(Uri uri, String methodName) {
443        int match = sURIMatcher.match(uri);
444        if (match < 0) {
445            throw new IllegalArgumentException("Unknown uri: " + uri);
446        } else if (Logging.LOGD) {
447            Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
448        }
449        return match;
450    }
451
452    private SQLiteDatabase mDatabase;
453    private SQLiteDatabase mBodyDatabase;
454
455    public static Uri uiUri(String type, long id) {
456        return Uri.parse(uiUriString(type, id));
457    }
458
459    public static String uiUriString(String type, long id) {
460        return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id));
461    }
462
463    /**
464     * Orphan record deletion utility.  Generates a sqlite statement like:
465     *  delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>)
466     * @param db the EmailProvider database
467     * @param table the table whose orphans are to be removed
468     * @param column the column deletion will be based on
469     * @param foreignColumn the column in the foreign table whose absence will trigger the deletion
470     * @param foreignTable the foreign table
471     */
472    @VisibleForTesting
473    void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn,
474            String foreignTable) {
475        int count = db.delete(table, column + " not in (select " + foreignColumn + " from " +
476                foreignTable + ")", null);
477        if (count > 0) {
478            Log.w(TAG, "Found " + count + " orphaned row(s) in " + table);
479        }
480    }
481
482    @VisibleForTesting
483    synchronized SQLiteDatabase getDatabase(Context context) {
484        // Always return the cached database, if we've got one
485        if (mDatabase != null) {
486            return mDatabase;
487        }
488
489        // Whenever we create or re-cache the databases, make sure that we haven't lost one
490        // to corruption
491        checkDatabases();
492
493        DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
494        mDatabase = helper.getWritableDatabase();
495        DBHelper.BodyDatabaseHelper bodyHelper =
496                new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME);
497        mBodyDatabase = bodyHelper.getWritableDatabase();
498        if (mBodyDatabase != null) {
499            String bodyFileName = mBodyDatabase.getPath();
500            mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
501        }
502
503        // Restore accounts if the database is corrupted...
504        restoreIfNeeded(context, mDatabase);
505
506        if (Email.DEBUG) {
507            Log.d(TAG, "Deleting orphans...");
508        }
509        // Check for any orphaned Messages in the updated/deleted tables
510        deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
511        deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME);
512        // Delete orphaned mailboxes/messages/policies (account no longer exists)
513        deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID,
514                Account.TABLE_NAME);
515        deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID,
516                Account.TABLE_NAME);
517        deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY,
518                Account.TABLE_NAME);
519
520        if (Email.DEBUG) {
521            Log.d(TAG, "EmailProvider pre-caching...");
522        }
523        preCacheData();
524        if (Email.DEBUG) {
525            Log.d(TAG, "EmailProvider ready.");
526        }
527        return mDatabase;
528    }
529
530    /**
531     * Pre-cache all of the items in a given table meeting the selection criteria
532     * @param tableUri the table uri
533     * @param baseProjection the base projection of that table
534     * @param selection the selection criteria
535     */
536    private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) {
537        Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null);
538        try {
539            while (c.moveToNext()) {
540                long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
541                Cursor cachedCursor = query(ContentUris.withAppendedId(
542                        tableUri, id), baseProjection, null, null, null);
543                if (cachedCursor != null) {
544                    // For accounts, create a mailbox type map entry (if necessary)
545                    if (tableUri == Account.CONTENT_URI) {
546                        getOrCreateAccountMailboxTypeMap(id);
547                    }
548                    cachedCursor.close();
549                }
550            }
551        } finally {
552            c.close();
553        }
554    }
555
556    private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap =
557        new HashMap<Long, HashMap<Integer, Long>>();
558
559    private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) {
560        synchronized(mMailboxTypeMap) {
561            HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId);
562            if (accountMailboxTypeMap == null) {
563                accountMailboxTypeMap = new HashMap<Integer, Long>();
564                mMailboxTypeMap.put(accountId, accountMailboxTypeMap);
565            }
566            return accountMailboxTypeMap;
567        }
568    }
569
570    private void addToMailboxTypeMap(Cursor c) {
571        long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN);
572        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
573        synchronized(mMailboxTypeMap) {
574            HashMap<Integer, Long> accountMailboxTypeMap =
575                getOrCreateAccountMailboxTypeMap(accountId);
576            accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN));
577        }
578    }
579
580    private long getMailboxIdFromMailboxTypeMap(long accountId, int type) {
581        synchronized(mMailboxTypeMap) {
582            HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId);
583            Long mailboxId = null;
584            if (accountMap != null) {
585                mailboxId = accountMap.get(type);
586            }
587            if (mailboxId == null) return Mailbox.NO_MAILBOX;
588            return mailboxId;
589        }
590    }
591
592    private void preCacheData() {
593        synchronized(mMailboxTypeMap) {
594            mMailboxTypeMap.clear();
595
596            // Pre-cache accounts, host auth's, policies, and special mailboxes
597            preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null);
598            preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null);
599            preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null);
600            preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
601                    MAILBOX_PRE_CACHE_SELECTION);
602
603            // Create a map from account,type to a mailbox
604            Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot();
605            Collection<Cursor> values = snapshot.values();
606            if (values != null) {
607                for (Cursor c: values) {
608                    if (c.moveToFirst()) {
609                        addToMailboxTypeMap(c);
610                    }
611                }
612            }
613        }
614    }
615
616    /*package*/ static SQLiteDatabase getReadableDatabase(Context context) {
617        DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
618        return helper.getReadableDatabase();
619    }
620
621    /**
622     * Restore user Account and HostAuth data from our backup database
623     */
624    public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
625        if (Email.DEBUG) {
626            Log.w(TAG, "restoreIfNeeded...");
627        }
628        // Check for legacy backup
629        String legacyBackup = Preferences.getLegacyBackupPreference(context);
630        // If there's a legacy backup, create a new-style backup and delete the legacy backup
631        // In the 1:1000000000 chance that the user gets an app update just as his database becomes
632        // corrupt, oh well...
633        if (!TextUtils.isEmpty(legacyBackup)) {
634            backupAccounts(context, mainDatabase);
635            Preferences.clearLegacyBackupPreference(context);
636            Log.w(TAG, "Created new EmailProvider backup database");
637            return;
638        }
639
640        // If we have accounts, we're done
641        Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null,
642                null, null, null);
643        if (c.moveToFirst()) {
644            if (Email.DEBUG) {
645                Log.w(TAG, "restoreIfNeeded: Account exists.");
646            }
647            return; // At least one account exists.
648        }
649        restoreAccounts(context, mainDatabase);
650    }
651
652    /** {@inheritDoc} */
653    @Override
654    public void shutdown() {
655        if (mDatabase != null) {
656            mDatabase.close();
657            mDatabase = null;
658        }
659        if (mBodyDatabase != null) {
660            mBodyDatabase.close();
661            mBodyDatabase = null;
662        }
663    }
664
665    /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) {
666        if (database != null) {
667            // We'll look at all of the items in the table; there won't be many typically
668            Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
669            // Usually, there will be nothing in these tables, so make a quick check
670            try {
671                if (c.getCount() == 0) return;
672                ArrayList<Long> foundMailboxes = new ArrayList<Long>();
673                ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
674                ArrayList<Long> deleteList = new ArrayList<Long>();
675                String[] bindArray = new String[1];
676                while (c.moveToNext()) {
677                    // Get the mailbox key and see if we've already found this mailbox
678                    // If so, we're fine
679                    long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
680                    // If we already know this mailbox doesn't exist, mark the message for deletion
681                    if (notFoundMailboxes.contains(mailboxId)) {
682                        deleteList.add(c.getLong(ORPHANS_ID));
683                    // If we don't know about this mailbox, we'll try to find it
684                    } else if (!foundMailboxes.contains(mailboxId)) {
685                        bindArray[0] = Long.toString(mailboxId);
686                        Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
687                                Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
688                        try {
689                            // If it exists, we'll add it to the "found" mailboxes
690                            if (boxCursor.moveToFirst()) {
691                                foundMailboxes.add(mailboxId);
692                            // Otherwise, we'll add to "not found" and mark the message for deletion
693                            } else {
694                                notFoundMailboxes.add(mailboxId);
695                                deleteList.add(c.getLong(ORPHANS_ID));
696                            }
697                        } finally {
698                            boxCursor.close();
699                        }
700                    }
701                }
702                // Now, delete the orphan messages
703                for (long messageId: deleteList) {
704                    bindArray[0] = Long.toString(messageId);
705                    database.delete(tableName, WHERE_ID, bindArray);
706                }
707            } finally {
708                c.close();
709            }
710        }
711    }
712
713    @Override
714    public int delete(Uri uri, String selection, String[] selectionArgs) {
715        final int match = findMatch(uri, "delete");
716        Context context = getContext();
717        // Pick the correct database for this operation
718        // If we're in a transaction already (which would happen during applyBatch), then the
719        // body database is already attached to the email database and any attempt to use the
720        // body database directly will result in a SQLiteException (the database is locked)
721        SQLiteDatabase db = getDatabase(context);
722        int table = match >> BASE_SHIFT;
723        String id = "0";
724        boolean messageDeletion = false;
725        ContentResolver resolver = context.getContentResolver();
726
727        ContentCache cache = mContentCaches[table];
728        String tableName = TABLE_NAMES[table];
729        int result = -1;
730
731        try {
732            if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
733                if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
734                    notifyUIConversation(uri);
735                }
736            }
737            switch (match) {
738                case UI_MESSAGE:
739                    return uiDeleteMessage(uri);
740                case UI_ACCOUNT_DATA:
741                    return uiDeleteAccountData(uri);
742                case UI_ACCOUNT:
743                    return uiDeleteAccount(uri);
744                // These are cases in which one or more Messages might get deleted, either by
745                // cascade or explicitly
746                case MAILBOX_ID:
747                case MAILBOX:
748                case ACCOUNT_ID:
749                case ACCOUNT:
750                case MESSAGE:
751                case SYNCED_MESSAGE_ID:
752                case MESSAGE_ID:
753                    // Handle lost Body records here, since this cannot be done in a trigger
754                    // The process is:
755                    //  1) Begin a transaction, ensuring that both databases are affected atomically
756                    //  2) Do the requested deletion, with cascading deletions handled in triggers
757                    //  3) End the transaction, committing all changes atomically
758                    //
759                    // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
760                    messageDeletion = true;
761                    db.beginTransaction();
762                    break;
763            }
764            switch (match) {
765                case BODY_ID:
766                case DELETED_MESSAGE_ID:
767                case SYNCED_MESSAGE_ID:
768                case MESSAGE_ID:
769                case UPDATED_MESSAGE_ID:
770                case ATTACHMENT_ID:
771                case MAILBOX_ID:
772                case ACCOUNT_ID:
773                case HOSTAUTH_ID:
774                case POLICY_ID:
775                case QUICK_RESPONSE_ID:
776                    id = uri.getPathSegments().get(1);
777                    if (match == SYNCED_MESSAGE_ID) {
778                        // For synced messages, first copy the old message to the deleted table and
779                        // delete it from the updated table (in case it was updated first)
780                        // Note that this is all within a transaction, for atomicity
781                        db.execSQL(DELETED_MESSAGE_INSERT + id);
782                        db.execSQL(UPDATED_MESSAGE_DELETE + id);
783                    }
784                    if (cache != null) {
785                        cache.lock(id);
786                    }
787                    try {
788                        result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
789                        if (cache != null) {
790                            switch(match) {
791                                case ACCOUNT_ID:
792                                    // Account deletion will clear all of the caches, as HostAuth's,
793                                    // Mailboxes, and Messages will be deleted in the process
794                                    mCacheMailbox.invalidate("Delete", uri, selection);
795                                    mCacheHostAuth.invalidate("Delete", uri, selection);
796                                    mCachePolicy.invalidate("Delete", uri, selection);
797                                    //$FALL-THROUGH$
798                                case MAILBOX_ID:
799                                    // Mailbox deletion will clear the Message cache
800                                    mCacheMessage.invalidate("Delete", uri, selection);
801                                    //$FALL-THROUGH$
802                                case SYNCED_MESSAGE_ID:
803                                case MESSAGE_ID:
804                                case HOSTAUTH_ID:
805                                case POLICY_ID:
806                                    cache.invalidate("Delete", uri, selection);
807                                    // Make sure all data is properly cached
808                                    if (match != MESSAGE_ID) {
809                                        preCacheData();
810                                    }
811                                    break;
812                            }
813                        }
814                    } finally {
815                        if (cache != null) {
816                            cache.unlock(id);
817                        }
818                    }
819                    break;
820                case ATTACHMENTS_MESSAGE_ID:
821                    // All attachments for the given message
822                    id = uri.getPathSegments().get(2);
823                    result = db.delete(tableName,
824                            whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
825                    break;
826
827                case BODY:
828                case MESSAGE:
829                case DELETED_MESSAGE:
830                case UPDATED_MESSAGE:
831                case ATTACHMENT:
832                case MAILBOX:
833                case ACCOUNT:
834                case HOSTAUTH:
835                case POLICY:
836                    switch(match) {
837                        // See the comments above for deletion of ACCOUNT_ID, etc
838                        case ACCOUNT:
839                            mCacheMailbox.invalidate("Delete", uri, selection);
840                            mCacheHostAuth.invalidate("Delete", uri, selection);
841                            mCachePolicy.invalidate("Delete", uri, selection);
842                            //$FALL-THROUGH$
843                        case MAILBOX:
844                            mCacheMessage.invalidate("Delete", uri, selection);
845                            //$FALL-THROUGH$
846                        case MESSAGE:
847                        case HOSTAUTH:
848                        case POLICY:
849                            cache.invalidate("Delete", uri, selection);
850                            break;
851                    }
852                    result = db.delete(tableName, selection, selectionArgs);
853                    switch(match) {
854                        case ACCOUNT:
855                        case MAILBOX:
856                        case HOSTAUTH:
857                        case POLICY:
858                            // Make sure all data is properly cached
859                            preCacheData();
860                            break;
861                    }
862                    break;
863
864                default:
865                    throw new IllegalArgumentException("Unknown URI " + uri);
866            }
867            if (messageDeletion) {
868                if (match == MESSAGE_ID) {
869                    // Delete the Body record associated with the deleted message
870                    db.execSQL(DELETE_BODY + id);
871                } else {
872                    // Delete any orphaned Body records
873                    db.execSQL(DELETE_ORPHAN_BODIES);
874                }
875                db.setTransactionSuccessful();
876            }
877        } catch (SQLiteException e) {
878            checkDatabases();
879            throw e;
880        } finally {
881            if (messageDeletion) {
882                db.endTransaction();
883            }
884        }
885
886        // Notify all notifier cursors
887        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
888
889        // Notify all email content cursors
890        resolver.notifyChange(EmailContent.CONTENT_URI, null);
891        return result;
892    }
893
894    @Override
895    // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
896    public String getType(Uri uri) {
897        int match = findMatch(uri, "getType");
898        switch (match) {
899            case BODY_ID:
900                return "vnd.android.cursor.item/email-body";
901            case BODY:
902                return "vnd.android.cursor.dir/email-body";
903            case UPDATED_MESSAGE_ID:
904            case MESSAGE_ID:
905                // NOTE: According to the framework folks, we're supposed to invent mime types as
906                // a way of passing information to drag & drop recipients.
907                // If there's a mailboxId parameter in the url, we respond with a mime type that
908                // has -n appended, where n is the mailboxId of the message.  The drag & drop code
909                // uses this information to know not to allow dragging the item to its own mailbox
910                String mimeType = EMAIL_MESSAGE_MIME_TYPE;
911                String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID);
912                if (mailboxId != null) {
913                    mimeType += "-" + mailboxId;
914                }
915                return mimeType;
916            case UPDATED_MESSAGE:
917            case MESSAGE:
918                return "vnd.android.cursor.dir/email-message";
919            case MAILBOX:
920                return "vnd.android.cursor.dir/email-mailbox";
921            case MAILBOX_ID:
922                return "vnd.android.cursor.item/email-mailbox";
923            case ACCOUNT:
924                return "vnd.android.cursor.dir/email-account";
925            case ACCOUNT_ID:
926                return "vnd.android.cursor.item/email-account";
927            case ATTACHMENTS_MESSAGE_ID:
928            case ATTACHMENT:
929                return "vnd.android.cursor.dir/email-attachment";
930            case ATTACHMENT_ID:
931                return EMAIL_ATTACHMENT_MIME_TYPE;
932            case HOSTAUTH:
933                return "vnd.android.cursor.dir/email-hostauth";
934            case HOSTAUTH_ID:
935                return "vnd.android.cursor.item/email-hostauth";
936            default:
937                throw new IllegalArgumentException("Unknown URI " + uri);
938        }
939    }
940
941    private static final Uri UIPROVIDER_CONVERSATION_NOTIFIER =
942            Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages");
943    private static final Uri UIPROVIDER_MAILBOX_NOTIFIER =
944            Uri.parse("content://" + UIProvider.AUTHORITY + "/uifolder");
945    private static final Uri UIPROVIDER_ACCOUNT_NOTIFIER =
946            Uri.parse("content://" + UIProvider.AUTHORITY + "/uiaccount");
947    private static final Uri UIPROVIDER_SETTINGS_NOTIFIER =
948            Uri.parse("content://" + UIProvider.AUTHORITY + "/uisettings");
949    private static final Uri UIPROVIDER_ATTACHMENT_NOTIFIER =
950            Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachment");
951    private static final Uri UIPROVIDER_ATTACHMENTS_NOTIFIER =
952            Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachments");
953
954    @Override
955    public Uri insert(Uri uri, ContentValues values) {
956        int match = findMatch(uri, "insert");
957        Context context = getContext();
958        ContentResolver resolver = context.getContentResolver();
959
960        // See the comment at delete(), above
961        SQLiteDatabase db = getDatabase(context);
962        int table = match >> BASE_SHIFT;
963        String id = "0";
964        long longId;
965
966        // We do NOT allow setting of unreadCount/messageCount via the provider
967        // These columns are maintained via triggers
968        if (match == MAILBOX_ID || match == MAILBOX) {
969            values.put(MailboxColumns.UNREAD_COUNT, 0);
970            values.put(MailboxColumns.MESSAGE_COUNT, 0);
971        }
972
973        Uri resultUri = null;
974
975        try {
976            switch (match) {
977                case UI_SAVEDRAFT:
978                    return uiSaveDraft(uri, values);
979                case UI_SENDMAIL:
980                    return uiSendMail(uri, values);
981                // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
982                // or DELETED_MESSAGE; see the comment below for details
983                case UPDATED_MESSAGE:
984                case DELETED_MESSAGE:
985                case MESSAGE:
986                case BODY:
987                case ATTACHMENT:
988                case MAILBOX:
989                case ACCOUNT:
990                case HOSTAUTH:
991                case POLICY:
992                case QUICK_RESPONSE:
993                    longId = db.insert(TABLE_NAMES[table], "foo", values);
994                    resultUri = ContentUris.withAppendedId(uri, longId);
995                    switch(match) {
996                        case MESSAGE:
997                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
998                                notifyUIConversationMailbox(values.getAsLong(Message.MAILBOX_KEY));
999                            }
1000                            break;
1001                        case MAILBOX:
1002                            if (values.containsKey(MailboxColumns.TYPE)) {
1003                                // Only cache special mailbox types
1004                                int type = values.getAsInteger(MailboxColumns.TYPE);
1005                                if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX &&
1006                                        type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT &&
1007                                        type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) {
1008                                    break;
1009                                }
1010                            }
1011                            //$FALL-THROUGH$
1012                        case ACCOUNT:
1013                        case HOSTAUTH:
1014                        case POLICY:
1015                            // Cache new account, host auth, policy, and some mailbox rows
1016                            Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null);
1017                            if (c != null) {
1018                                if (match == MAILBOX) {
1019                                    addToMailboxTypeMap(c);
1020                                } else if (match == ACCOUNT) {
1021                                    getOrCreateAccountMailboxTypeMap(longId);
1022                                }
1023                                c.close();
1024                            }
1025                            break;
1026                    }
1027                    // Clients shouldn't normally be adding rows to these tables, as they are
1028                    // maintained by triggers.  However, we need to be able to do this for unit
1029                    // testing, so we allow the insert and then throw the same exception that we
1030                    // would if this weren't allowed.
1031                    if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) {
1032                        throw new IllegalArgumentException("Unknown URL " + uri);
1033                    }
1034                    if (match == ATTACHMENT) {
1035                        int flags = 0;
1036                        if (values.containsKey(Attachment.FLAGS)) {
1037                            flags = values.getAsInteger(Attachment.FLAGS);
1038                        }
1039                        // Report all new attachments to the download service
1040                        mAttachmentService.attachmentChanged(getContext(), longId, flags);
1041                    }
1042                    break;
1043                case MAILBOX_ID:
1044                    // This implies adding a message to a mailbox
1045                    // Hmm, a problem here is that we can't link the account as well, so it must be
1046                    // already in the values...
1047                    longId = Long.parseLong(uri.getPathSegments().get(1));
1048                    values.put(MessageColumns.MAILBOX_KEY, longId);
1049                    return insert(Message.CONTENT_URI, values); // Recurse
1050                case MESSAGE_ID:
1051                    // This implies adding an attachment to a message.
1052                    id = uri.getPathSegments().get(1);
1053                    longId = Long.parseLong(id);
1054                    values.put(AttachmentColumns.MESSAGE_KEY, longId);
1055                    return insert(Attachment.CONTENT_URI, values); // Recurse
1056                case ACCOUNT_ID:
1057                    // This implies adding a mailbox to an account.
1058                    longId = Long.parseLong(uri.getPathSegments().get(1));
1059                    values.put(MailboxColumns.ACCOUNT_KEY, longId);
1060                    return insert(Mailbox.CONTENT_URI, values); // Recurse
1061                case ATTACHMENTS_MESSAGE_ID:
1062                    longId = db.insert(TABLE_NAMES[table], "foo", values);
1063                    resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
1064                    break;
1065                default:
1066                    throw new IllegalArgumentException("Unknown URL " + uri);
1067            }
1068        } catch (SQLiteException e) {
1069            checkDatabases();
1070            throw e;
1071        }
1072
1073        // Notify all notifier cursors
1074        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
1075
1076        // Notify all existing cursors.
1077        resolver.notifyChange(EmailContent.CONTENT_URI, null);
1078        return resultUri;
1079    }
1080
1081    @Override
1082    public boolean onCreate() {
1083        checkDatabases();
1084        return false;
1085    }
1086
1087    /**
1088     * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
1089     * always be in sync (i.e. there are two database or NO databases).  This code will delete
1090     * any "orphan" database, so that both will be created together.  Note that an "orphan" database
1091     * will exist after either of the individual databases is deleted due to data corruption.
1092     */
1093    public void checkDatabases() {
1094        // Uncache the databases
1095        if (mDatabase != null) {
1096            mDatabase = null;
1097        }
1098        if (mBodyDatabase != null) {
1099            mBodyDatabase = null;
1100        }
1101        // Look for orphans, and delete as necessary; these must always be in sync
1102        File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
1103        File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
1104
1105        // TODO Make sure attachments are deleted
1106        if (databaseFile.exists() && !bodyFile.exists()) {
1107            Log.w(TAG, "Deleting orphaned EmailProvider database...");
1108            databaseFile.delete();
1109        } else if (bodyFile.exists() && !databaseFile.exists()) {
1110            Log.w(TAG, "Deleting orphaned EmailProviderBody database...");
1111            bodyFile.delete();
1112        }
1113    }
1114    @Override
1115    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1116            String sortOrder) {
1117        long time = 0L;
1118        if (Email.DEBUG) {
1119            time = System.nanoTime();
1120        }
1121        Cursor c = null;
1122        int match;
1123        try {
1124            match = findMatch(uri, "query");
1125        } catch (IllegalArgumentException e) {
1126            String uriString = uri.toString();
1127            // If we were passed an illegal uri, see if it ends in /-1
1128            // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
1129            if (uriString != null && uriString.endsWith("/-1")) {
1130                uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
1131                match = findMatch(uri, "query");
1132                switch (match) {
1133                    case BODY_ID:
1134                    case MESSAGE_ID:
1135                    case DELETED_MESSAGE_ID:
1136                    case UPDATED_MESSAGE_ID:
1137                    case ATTACHMENT_ID:
1138                    case MAILBOX_ID:
1139                    case ACCOUNT_ID:
1140                    case HOSTAUTH_ID:
1141                    case POLICY_ID:
1142                        return new MatrixCursor(projection, 0);
1143                }
1144            }
1145            throw e;
1146        }
1147        Context context = getContext();
1148        // See the comment at delete(), above
1149        SQLiteDatabase db = getDatabase(context);
1150        int table = match >> BASE_SHIFT;
1151        String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
1152        String id;
1153
1154        // Find the cache for this query's table (if any)
1155        ContentCache cache = null;
1156        String tableName = TABLE_NAMES[table];
1157        // We can only use the cache if there's no selection
1158        if (selection == null) {
1159            cache = mContentCaches[table];
1160        }
1161        if (cache == null) {
1162            ContentCache.notCacheable(uri, selection);
1163        }
1164
1165        try {
1166            switch (match) {
1167                // First, dispatch queries from UnfiedEmail
1168                case UI_SEARCH:
1169                    return uiSearch(uri, projection);
1170                case UI_ACCTS:
1171                    c = uiAccounts(projection);
1172                    return c;
1173                case UI_UNDO:
1174                    return uiUndo(uri, projection);
1175                case UI_SUBFOLDERS:
1176                case UI_FOLDERS:
1177                case UI_MESSAGES:
1178                case UI_MESSAGE:
1179                case UI_FOLDER:
1180                case UI_ACCOUNT:
1181                case UI_SETTINGS:
1182                case UI_ATTACHMENT:
1183                case UI_ATTACHMENTS:
1184                    // For now, we don't allow selection criteria within these queries
1185                    if (selection != null || selectionArgs != null) {
1186                        throw new IllegalArgumentException("UI queries can't have selection/args");
1187                    }
1188                    c = uiQuery(match, uri, projection);
1189                    return c;
1190                case UI_FOLDER_LOAD_MORE:
1191                    c = uiFolderLoadMore(uri);
1192                    return c;
1193                case UI_FOLDER_REFRESH:
1194                    c = uiFolderRefresh(uri);
1195                    return c;
1196                case MAILBOX_NOTIFICATION:
1197                    c = notificationQuery(uri);
1198                    return c;
1199                case MAILBOX_MOST_RECENT_MESSAGE:
1200                    c = mostRecentMessageQuery(uri);
1201                    return c;
1202                case ACCOUNT_DEFAULT_ID:
1203                    // Start with a snapshot of the cache
1204                    Map<String, Cursor> accountCache = mCacheAccount.getSnapshot();
1205                    long accountId = Account.NO_ACCOUNT;
1206                    // Find the account with "isDefault" set, or the lowest account ID otherwise.
1207                    // Note that the snapshot from the cached isn't guaranteed to be sorted in any
1208                    // way.
1209                    Collection<Cursor> accounts = accountCache.values();
1210                    for (Cursor accountCursor: accounts) {
1211                        // For now, at least, we can have zero count cursors (e.g. if someone looks
1212                        // up a non-existent id); we need to skip these
1213                        if (accountCursor.moveToFirst()) {
1214                            boolean isDefault =
1215                                accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1;
1216                            long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN);
1217                            // We'll remember this one if it's the default or the first one we see
1218                            if (isDefault) {
1219                                accountId = iterId;
1220                                break;
1221                            } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) {
1222                                accountId = iterId;
1223                            }
1224                        }
1225                    }
1226                    // Return a cursor with an id projection
1227                    MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION);
1228                    mc.addRow(new Object[] {accountId});
1229                    c = mc;
1230                    break;
1231                case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE:
1232                    // Get accountId and type and find the mailbox in our map
1233                    List<String> pathSegments = uri.getPathSegments();
1234                    accountId = Long.parseLong(pathSegments.get(1));
1235                    int type = Integer.parseInt(pathSegments.get(2));
1236                    long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type);
1237                    // Return a cursor with an id projection
1238                    mc = new MatrixCursor(EmailContent.ID_PROJECTION);
1239                    mc.addRow(new Object[] {mailboxId});
1240                    c = mc;
1241                    break;
1242                case BODY:
1243                case MESSAGE:
1244                case UPDATED_MESSAGE:
1245                case DELETED_MESSAGE:
1246                case ATTACHMENT:
1247                case MAILBOX:
1248                case ACCOUNT:
1249                case HOSTAUTH:
1250                case POLICY:
1251                case QUICK_RESPONSE:
1252                    // Special-case "count of accounts"; it's common and we always know it
1253                    if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) &&
1254                            selection == null && limit.equals("1")) {
1255                        int accountCount = mMailboxTypeMap.size();
1256                        // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this
1257                        if (accountCount < MAX_CACHED_ACCOUNTS) {
1258                            mc = new MatrixCursor(projection, 1);
1259                            mc.addRow(new Object[] {accountCount});
1260                            c = mc;
1261                            break;
1262                        }
1263                    }
1264                    c = db.query(tableName, projection,
1265                            selection, selectionArgs, null, null, sortOrder, limit);
1266                    break;
1267                case BODY_ID:
1268                case MESSAGE_ID:
1269                case DELETED_MESSAGE_ID:
1270                case UPDATED_MESSAGE_ID:
1271                case ATTACHMENT_ID:
1272                case MAILBOX_ID:
1273                case ACCOUNT_ID:
1274                case HOSTAUTH_ID:
1275                case POLICY_ID:
1276                case QUICK_RESPONSE_ID:
1277                    id = uri.getPathSegments().get(1);
1278                    if (cache != null) {
1279                        c = cache.getCachedCursor(id, projection);
1280                    }
1281                    if (c == null) {
1282                        CacheToken token = null;
1283                        if (cache != null) {
1284                            token = cache.getCacheToken(id);
1285                        }
1286                        c = db.query(tableName, projection, whereWithId(id, selection),
1287                                selectionArgs, null, null, sortOrder, limit);
1288                        if (cache != null) {
1289                            c = cache.putCursor(c, id, projection, token);
1290                        }
1291                    }
1292                    break;
1293                case ATTACHMENTS_MESSAGE_ID:
1294                    // All attachments for the given message
1295                    id = uri.getPathSegments().get(2);
1296                    c = db.query(Attachment.TABLE_NAME, projection,
1297                            whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
1298                            selectionArgs, null, null, sortOrder, limit);
1299                    break;
1300                case QUICK_RESPONSE_ACCOUNT_ID:
1301                    // All quick responses for the given account
1302                    id = uri.getPathSegments().get(2);
1303                    c = db.query(QuickResponse.TABLE_NAME, projection,
1304                            whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection),
1305                            selectionArgs, null, null, sortOrder);
1306                    break;
1307                default:
1308                    throw new IllegalArgumentException("Unknown URI " + uri);
1309            }
1310        } catch (SQLiteException e) {
1311            checkDatabases();
1312            throw e;
1313        } catch (RuntimeException e) {
1314            checkDatabases();
1315            e.printStackTrace();
1316            throw e;
1317        } finally {
1318            if (cache != null && c != null && Email.DEBUG) {
1319                cache.recordQueryTime(c, System.nanoTime() - time);
1320            }
1321            if (c == null) {
1322                // This should never happen, but let's be sure to log it...
1323                Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection);
1324            }
1325        }
1326
1327        if ((c != null) && !isTemporary()) {
1328            c.setNotificationUri(getContext().getContentResolver(), uri);
1329        }
1330        return c;
1331    }
1332
1333    private String whereWithId(String id, String selection) {
1334        StringBuilder sb = new StringBuilder(256);
1335        sb.append("_id=");
1336        sb.append(id);
1337        if (selection != null) {
1338            sb.append(" AND (");
1339            sb.append(selection);
1340            sb.append(')');
1341        }
1342        return sb.toString();
1343    }
1344
1345    /**
1346     * Combine a locally-generated selection with a user-provided selection
1347     *
1348     * This introduces risk that the local selection might insert incorrect chars
1349     * into the SQL, so use caution.
1350     *
1351     * @param where locally-generated selection, must not be null
1352     * @param selection user-provided selection, may be null
1353     * @return a single selection string
1354     */
1355    private String whereWith(String where, String selection) {
1356        if (selection == null) {
1357            return where;
1358        }
1359        StringBuilder sb = new StringBuilder(where);
1360        sb.append(" AND (");
1361        sb.append(selection);
1362        sb.append(')');
1363
1364        return sb.toString();
1365    }
1366
1367    /**
1368     * Restore a HostAuth from a database, given its unique id
1369     * @param db the database
1370     * @param id the unique id (_id) of the row
1371     * @return a fully populated HostAuth or null if the row does not exist
1372     */
1373    private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
1374        Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
1375                HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null);
1376        try {
1377            if (c.moveToFirst()) {
1378                HostAuth hostAuth = new HostAuth();
1379                hostAuth.restore(c);
1380                return hostAuth;
1381            }
1382            return null;
1383        } finally {
1384            c.close();
1385        }
1386    }
1387
1388    /**
1389     * Copy the Account and HostAuth tables from one database to another
1390     * @param fromDatabase the source database
1391     * @param toDatabase the destination database
1392     * @return the number of accounts copied, or -1 if an error occurred
1393     */
1394    private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
1395        if (fromDatabase == null || toDatabase == null) return -1;
1396        int copyCount = 0;
1397        try {
1398            // Lock both databases; for the "from" database, we don't want anyone changing it from
1399            // under us; for the "to" database, we want to make the operation atomic
1400            fromDatabase.beginTransaction();
1401            toDatabase.beginTransaction();
1402            // Delete anything hanging around here
1403            toDatabase.delete(Account.TABLE_NAME, null, null);
1404            toDatabase.delete(HostAuth.TABLE_NAME, null, null);
1405            // Get our account cursor
1406            Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
1407                    null, null, null, null, null);
1408            boolean noErrors = true;
1409            try {
1410                // Loop through accounts, copying them and associated host auth's
1411                while (c.moveToNext()) {
1412                    Account account = new Account();
1413                    account.restore(c);
1414
1415                    // Clear security sync key and sync key, as these were specific to the state of
1416                    // the account, and we've reset that...
1417                    // Clear policy key so that we can re-establish policies from the server
1418                    // TODO This is pretty EAS specific, but there's a lot of that around
1419                    account.mSecuritySyncKey = null;
1420                    account.mSyncKey = null;
1421                    account.mPolicyKey = 0;
1422
1423                    // Copy host auth's and update foreign keys
1424                    HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv);
1425                    // The account might have gone away, though very unlikely
1426                    if (hostAuth == null) continue;
1427                    account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
1428                            hostAuth.toContentValues());
1429                    // EAS accounts have no send HostAuth
1430                    if (account.mHostAuthKeySend > 0) {
1431                        hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
1432                        // Belt and suspenders; I can't imagine that this is possible, since we
1433                        // checked the validity of the account above, and the database is now locked
1434                        if (hostAuth == null) continue;
1435                        account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null,
1436                                hostAuth.toContentValues());
1437                    }
1438                    // Now, create the account in the "to" database
1439                    toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
1440                    copyCount++;
1441                }
1442            } catch (SQLiteException e) {
1443                noErrors = false;
1444                copyCount = -1;
1445            } finally {
1446                fromDatabase.endTransaction();
1447                if (noErrors) {
1448                    // Say it's ok to commit
1449                    toDatabase.setTransactionSuccessful();
1450                }
1451                toDatabase.endTransaction();
1452                c.close();
1453            }
1454        } catch (SQLiteException e) {
1455            copyCount = -1;
1456        }
1457        return copyCount;
1458    }
1459
1460    private static SQLiteDatabase getBackupDatabase(Context context) {
1461        DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME);
1462        return helper.getWritableDatabase();
1463    }
1464
1465    /**
1466     * Backup account data, returning the number of accounts backed up
1467     */
1468    private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
1469        if (Email.DEBUG) {
1470            Log.d(TAG, "backupAccounts...");
1471        }
1472        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1473        try {
1474            int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
1475            if (numBackedUp < 0) {
1476                Log.e(TAG, "Account backup failed!");
1477            } else if (Email.DEBUG) {
1478                Log.d(TAG, "Backed up " + numBackedUp + " accounts...");
1479            }
1480            return numBackedUp;
1481        } finally {
1482            if (backupDatabase != null) {
1483                backupDatabase.close();
1484            }
1485        }
1486    }
1487
1488    /**
1489     * Restore account data, returning the number of accounts restored
1490     */
1491    private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
1492        if (Email.DEBUG) {
1493            Log.d(TAG, "restoreAccounts...");
1494        }
1495        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1496        try {
1497            int numRecovered = copyAccountTables(backupDatabase, mainDatabase);
1498            if (numRecovered > 0) {
1499                Log.e(TAG, "Recovered " + numRecovered + " accounts!");
1500            } else if (numRecovered < 0) {
1501                Log.e(TAG, "Account recovery failed?");
1502            } else if (Email.DEBUG) {
1503                Log.d(TAG, "No accounts to restore...");
1504            }
1505            return numRecovered;
1506        } finally {
1507            if (backupDatabase != null) {
1508                backupDatabase.close();
1509            }
1510        }
1511    }
1512
1513    @Override
1514    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1515        // Handle this special case the fastest possible way
1516        if (uri == INTEGRITY_CHECK_URI) {
1517            checkDatabases();
1518            return 0;
1519        } else if (uri == ACCOUNT_BACKUP_URI) {
1520            return backupAccounts(getContext(), getDatabase(getContext()));
1521        }
1522
1523        // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
1524        Uri notificationUri = EmailContent.CONTENT_URI;
1525
1526        int match = findMatch(uri, "update");
1527        Context context = getContext();
1528        ContentResolver resolver = context.getContentResolver();
1529        // See the comment at delete(), above
1530        SQLiteDatabase db = getDatabase(context);
1531        int table = match >> BASE_SHIFT;
1532        int result;
1533
1534        // We do NOT allow setting of unreadCount/messageCount via the provider
1535        // These columns are maintained via triggers
1536        if (match == MAILBOX_ID || match == MAILBOX) {
1537            values.remove(MailboxColumns.UNREAD_COUNT);
1538            values.remove(MailboxColumns.MESSAGE_COUNT);
1539        }
1540
1541        ContentCache cache = mContentCaches[table];
1542        String tableName = TABLE_NAMES[table];
1543        String id = "0";
1544
1545        try {
1546            if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
1547                if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
1548                    notifyUIConversation(uri);
1549                }
1550            }
1551outer:
1552            switch (match) {
1553                case UI_ATTACHMENT:
1554                    return uiUpdateAttachment(uri, values);
1555                case UI_UPDATEDRAFT:
1556                    return uiUpdateDraft(uri, values);
1557                case UI_SENDDRAFT:
1558                    return uiSendDraft(uri, values);
1559                case UI_MESSAGE:
1560                    return uiUpdateMessage(uri, values);
1561                case MAILBOX_ID_ADD_TO_FIELD:
1562                case ACCOUNT_ID_ADD_TO_FIELD:
1563                    id = uri.getPathSegments().get(1);
1564                    String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME);
1565                    Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME);
1566                    if (field == null || add == null) {
1567                        throw new IllegalArgumentException("No field/add specified " + uri);
1568                    }
1569                    ContentValues actualValues = new ContentValues();
1570                    if (cache != null) {
1571                        cache.lock(id);
1572                    }
1573                    try {
1574                        db.beginTransaction();
1575                        try {
1576                            Cursor c = db.query(tableName,
1577                                    new String[] {EmailContent.RECORD_ID, field},
1578                                    whereWithId(id, selection),
1579                                    selectionArgs, null, null, null);
1580                            try {
1581                                result = 0;
1582                                String[] bind = new String[1];
1583                                if (c.moveToNext()) {
1584                                    bind[0] = c.getString(0); // _id
1585                                    long value = c.getLong(1) + add;
1586                                    actualValues.put(field, value);
1587                                    result = db.update(tableName, actualValues, ID_EQUALS, bind);
1588                                }
1589                                db.setTransactionSuccessful();
1590                            } finally {
1591                                c.close();
1592                            }
1593                        } finally {
1594                            db.endTransaction();
1595                        }
1596                    } finally {
1597                        if (cache != null) {
1598                            cache.unlock(id, actualValues);
1599                        }
1600                    }
1601                    break;
1602                case SYNCED_MESSAGE_ID:
1603                case UPDATED_MESSAGE_ID:
1604                case MESSAGE_ID:
1605                case BODY_ID:
1606                case ATTACHMENT_ID:
1607                case MAILBOX_ID:
1608                case ACCOUNT_ID:
1609                case HOSTAUTH_ID:
1610                case QUICK_RESPONSE_ID:
1611                case POLICY_ID:
1612                    id = uri.getPathSegments().get(1);
1613                    if (cache != null) {
1614                        cache.lock(id);
1615                    }
1616                    try {
1617                        if (match == SYNCED_MESSAGE_ID) {
1618                            // For synced messages, first copy the old message to the updated table
1619                            // Note the insert or ignore semantics, guaranteeing that only the first
1620                            // update will be reflected in the updated message table; therefore this
1621                            // row will always have the "original" data
1622                            db.execSQL(UPDATED_MESSAGE_INSERT + id);
1623                        } else if (match == MESSAGE_ID) {
1624                            db.execSQL(UPDATED_MESSAGE_DELETE + id);
1625                        }
1626                        result = db.update(tableName, values, whereWithId(id, selection),
1627                                selectionArgs);
1628                    } catch (SQLiteException e) {
1629                        // Null out values (so they aren't cached) and re-throw
1630                        values = null;
1631                        throw e;
1632                    } finally {
1633                        if (cache != null) {
1634                            cache.unlock(id, values);
1635                        }
1636                    }
1637                    if (match == ATTACHMENT_ID) {
1638                        long attId = Integer.parseInt(id);
1639                        if (values.containsKey(Attachment.FLAGS)) {
1640                            int flags = values.getAsInteger(Attachment.FLAGS);
1641                            mAttachmentService.attachmentChanged(context, attId, flags);
1642                        }
1643                        // Notify UI if necessary; there are only two columns we can change that
1644                        // would be worth a notification
1645                        if (values.containsKey(AttachmentColumns.UI_STATE) ||
1646                                values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
1647                            // Notify on individual attachment
1648                            notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
1649                            Attachment att = Attachment.restoreAttachmentWithId(context, attId);
1650                            if (att != null) {
1651                                // And on owning Message
1652                                notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
1653                            }
1654                        }
1655                    } else if (match == MAILBOX_ID && values.containsKey(Mailbox.UI_SYNC_STATUS)) {
1656                        notifyUI(UIPROVIDER_MAILBOX_NOTIFIER, id);
1657                        // TODO: Remove logging
1658                        Log.d(TAG, "Notifying mailbox " + id + " status: " +
1659                                values.getAsInteger(Mailbox.UI_SYNC_STATUS));
1660                    } else if (match == ACCOUNT_ID) {
1661                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
1662                    }
1663                    break;
1664                case BODY:
1665                case MESSAGE:
1666                case UPDATED_MESSAGE:
1667                case ATTACHMENT:
1668                case MAILBOX:
1669                case ACCOUNT:
1670                case HOSTAUTH:
1671                case POLICY:
1672                    switch(match) {
1673                        // To avoid invalidating the cache on updates, we execute them one at a
1674                        // time using the XXX_ID uri; these are all executed atomically
1675                        case ACCOUNT:
1676                        case MAILBOX:
1677                        case HOSTAUTH:
1678                        case POLICY:
1679                            Cursor c = db.query(tableName, EmailContent.ID_PROJECTION,
1680                                    selection, selectionArgs, null, null, null);
1681                            db.beginTransaction();
1682                            result = 0;
1683                            try {
1684                                while (c.moveToNext()) {
1685                                    update(ContentUris.withAppendedId(
1686                                                uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)),
1687                                            values, null, null);
1688                                    result++;
1689                                }
1690                                db.setTransactionSuccessful();
1691                            } finally {
1692                                db.endTransaction();
1693                                c.close();
1694                            }
1695                            break outer;
1696                        // Any cached table other than those above should be invalidated here
1697                        case MESSAGE:
1698                            // If we're doing some generic update, the whole cache needs to be
1699                            // invalidated.  This case should be quite rare
1700                            cache.invalidate("Update", uri, selection);
1701                            //$FALL-THROUGH$
1702                        default:
1703                            result = db.update(tableName, values, selection, selectionArgs);
1704                            break outer;
1705                    }
1706                case ACCOUNT_RESET_NEW_COUNT_ID:
1707                    id = uri.getPathSegments().get(1);
1708                    if (cache != null) {
1709                        cache.lock(id);
1710                    }
1711                    ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
1712                    if (values != null) {
1713                        Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME);
1714                        if (set != null) {
1715                            newMessageCount = new ContentValues();
1716                            newMessageCount.put(Account.NEW_MESSAGE_COUNT, set);
1717                        }
1718                    }
1719                    try {
1720                        result = db.update(tableName, newMessageCount,
1721                                whereWithId(id, selection), selectionArgs);
1722                    } finally {
1723                        if (cache != null) {
1724                            cache.unlock(id, values);
1725                        }
1726                    }
1727                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1728                    break;
1729                case ACCOUNT_RESET_NEW_COUNT:
1730                    result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
1731                            selection, selectionArgs);
1732                    // Affects all accounts.  Just invalidate all account cache.
1733                    cache.invalidate("Reset all new counts", null, null);
1734                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1735                    break;
1736                default:
1737                    throw new IllegalArgumentException("Unknown URI " + uri);
1738            }
1739        } catch (SQLiteException e) {
1740            checkDatabases();
1741            throw e;
1742        }
1743
1744        // Notify all notifier cursors
1745        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
1746
1747        resolver.notifyChange(notificationUri, null);
1748        return result;
1749    }
1750
1751    /**
1752     * Returns the base notification URI for the given content type.
1753     *
1754     * @param match The type of content that was modified.
1755     */
1756    private Uri getBaseNotificationUri(int match) {
1757        Uri baseUri = null;
1758        switch (match) {
1759            case MESSAGE:
1760            case MESSAGE_ID:
1761            case SYNCED_MESSAGE_ID:
1762                baseUri = Message.NOTIFIER_URI;
1763                break;
1764            case ACCOUNT:
1765            case ACCOUNT_ID:
1766                baseUri = Account.NOTIFIER_URI;
1767                break;
1768        }
1769        return baseUri;
1770    }
1771
1772    /**
1773     * Sends a change notification to any cursors observers of the given base URI. The final
1774     * notification URI is dynamically built to contain the specified information. It will be
1775     * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
1776     * upon the given values.
1777     * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
1778     * If this is necessary, it can be added. However, due to the implementation of
1779     * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
1780     *
1781     * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
1782     * @param op Optional operation to be appended to the URI.
1783     * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
1784     *           appended to the base URI.
1785     */
1786    private void sendNotifierChange(Uri baseUri, String op, String id) {
1787        if (baseUri == null) return;
1788
1789        final ContentResolver resolver = getContext().getContentResolver();
1790
1791        // Append the operation, if specified
1792        if (op != null) {
1793            baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
1794        }
1795
1796        long longId = 0L;
1797        try {
1798            longId = Long.valueOf(id);
1799        } catch (NumberFormatException ignore) {}
1800        if (longId > 0) {
1801            resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null);
1802        } else {
1803            resolver.notifyChange(baseUri, null);
1804        }
1805
1806        // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
1807        if (baseUri.equals(Message.NOTIFIER_URI)) {
1808            sendMessageListDataChangedNotification();
1809        }
1810    }
1811
1812    private void sendMessageListDataChangedNotification() {
1813        final Context context = getContext();
1814        final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
1815        // Ideally this intent would contain information about which account changed, to limit the
1816        // updates to that particular account.  Unfortunately, that information is not available in
1817        // sendNotifierChange().
1818        context.sendBroadcast(intent);
1819    }
1820
1821    @Override
1822    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1823            throws OperationApplicationException {
1824        Context context = getContext();
1825        SQLiteDatabase db = getDatabase(context);
1826        db.beginTransaction();
1827        try {
1828            ContentProviderResult[] results = super.applyBatch(operations);
1829            db.setTransactionSuccessful();
1830            return results;
1831        } finally {
1832            db.endTransaction();
1833        }
1834    }
1835
1836    /**
1837     * For testing purposes, check whether a given row is cached
1838     * @param baseUri the base uri of the EmailContent
1839     * @param id the row id of the EmailContent
1840     * @return whether or not the row is currently cached
1841     */
1842    @VisibleForTesting
1843    protected boolean isCached(Uri baseUri, long id) {
1844        int match = findMatch(baseUri, "isCached");
1845        int table = match >> BASE_SHIFT;
1846        ContentCache cache = mContentCaches[table];
1847        if (cache == null) return false;
1848        Cursor cc = cache.get(Long.toString(id));
1849        return (cc != null);
1850    }
1851
1852    public static interface AttachmentService {
1853        /**
1854         * Notify the service that an attachment has changed.
1855         */
1856        void attachmentChanged(Context context, long id, int flags);
1857    }
1858
1859    private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() {
1860        @Override
1861        public void attachmentChanged(Context context, long id, int flags) {
1862            // The default implementation delegates to the real service.
1863            AttachmentDownloadService.attachmentChanged(context, id, flags);
1864        }
1865    };
1866    private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
1867
1868    /**
1869     * Injects a custom attachment service handler. If null is specified, will reset to the
1870     * default service.
1871     */
1872    public void injectAttachmentService(AttachmentService as) {
1873        mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as;
1874    }
1875
1876    // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message,
1877    //   (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey
1878    //   FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1)))
1879    //      AS Boxes
1880    // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount
1881    //   OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey)
1882    // TODO: This query can be simplified a bit
1883    private static final String NOTIFICATION_QUERY =
1884        "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT +
1885            ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" +
1886        " FROM " +
1887            Message.TABLE_NAME + "," +
1888            "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," +
1889                MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT +
1890                "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME +
1891                " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" +
1892                " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR ("
1893                + MailboxColumns.SYNC_INTERVAL + "!=0 AND " +
1894                MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " +
1895        "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." +
1896                MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." +
1897                MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY +
1898                " AND " + MessageColumns.FLAG_READ + "=0";
1899
1900    public Cursor notificationQuery(Uri uri) {
1901        SQLiteDatabase db = getDatabase(getContext());
1902        String accountId = uri.getLastPathSegment();
1903        return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId});
1904   }
1905
1906    public Cursor mostRecentMessageQuery(Uri uri) {
1907        SQLiteDatabase db = getDatabase(getContext());
1908        String mailboxId = uri.getLastPathSegment();
1909        return db.rawQuery("select max(_id) from Message where mailboxKey=?",
1910                new String[] {mailboxId});
1911   }
1912
1913    /**
1914     * Support for UnifiedEmail below
1915     */
1916
1917    private static final String NOT_A_DRAFT_STRING =
1918        Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
1919
1920    /**
1921     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
1922     * conversation list in UnifiedEmail)
1923     */
1924    private static final ProjectionMap sMessageListMap = ProjectionMap.builder()
1925        .add(BaseColumns._ID, MessageColumns.ID)
1926        .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
1927        .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
1928        .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
1929        .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
1930        .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
1931        .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
1932        .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
1933        .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
1934        .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
1935        .add(UIProvider.ConversationColumns.SENDING_STATE,
1936                Integer.toString(ConversationSendingState.OTHER))
1937        .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW))
1938        .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
1939        .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
1940        .add(UIProvider.ConversationColumns.FOLDER_LIST, MessageColumns.MAILBOX_KEY)
1941        .build();
1942
1943    /**
1944     * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
1945     * UnifiedEmail
1946     */
1947    private static final ProjectionMap sMessageViewMap = ProjectionMap.builder()
1948        .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID)
1949        .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
1950        .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
1951        .add(UIProvider.MessageColumns.CONVERSATION_ID,
1952                uriWithFQId("uimessage", Message.TABLE_NAME))
1953        .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT)
1954        .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET)
1955        .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST)
1956        .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST)
1957        .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST)
1958        .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST)
1959        .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST)
1960        .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP)
1961        .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT)
1962        .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT)
1963        .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0")
1964        .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
1965        .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
1966        .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
1967        .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT)
1968        .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
1969                uriWithFQId("uiattachments", Message.TABLE_NAME))
1970        .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0")
1971        .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI,
1972                uriWithFQId("uiupdatedraft", Message.TABLE_NAME))
1973        .add(UIProvider.MessageColumns.SEND_MESSAGE_URI,
1974                uriWithFQId("uisenddraft", Message.TABLE_NAME))
1975        // TODO(pwestbro): make this actually return valid results.
1976        .add(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, "0")
1977        .build();
1978
1979    /**
1980     * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail
1981     */
1982    private static String getFolderCapabilities() {
1983        return "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
1984                ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
1985                " ELSE 0 END";
1986    }
1987
1988    private static final ProjectionMap sFolderListMap = ProjectionMap.builder()
1989        .add(BaseColumns._ID, MailboxColumns.ID)
1990        .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
1991        .add(UIProvider.FolderColumns.NAME, "displayName")
1992        .add(UIProvider.FolderColumns.HAS_CHILDREN,
1993                MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
1994        .add(UIProvider.FolderColumns.CAPABILITIES, getFolderCapabilities())
1995        .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
1996        .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
1997        .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
1998        .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
1999        .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT)
2000        .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh"))
2001        .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
2002        .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
2003        .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.TOTAL_COUNT)
2004        .build();
2005
2006    private static final ProjectionMap sAccountListMap = ProjectionMap.builder()
2007        .add(BaseColumns._ID, AccountColumns.ID)
2008        .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
2009        .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
2010        .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft"))
2011        .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail"))
2012        .add(UIProvider.AccountColumns.UNDO_URI, uriWithId("uiundo"))
2013        .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
2014        .add(UIProvider.AccountColumns.SETTINGS_QUERY_URI, uriWithId("uisettings"))
2015        .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
2016        // TODO: Is this used?
2017        .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
2018        .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
2019        .build();
2020
2021    /**
2022     * The "ORDER BY" clause for top level folders
2023     */
2024    private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
2025        + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
2026        + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
2027        + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
2028        + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
2029        + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
2030        + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
2031        // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
2032        + " ELSE 10 END"
2033        + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
2034
2035
2036    /**
2037     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
2038     * conversation list in UnifiedEmail)
2039     */
2040    private static final ProjectionMap sAccountSettingsMap = ProjectionMap.builder()
2041        .add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
2042        .add(UIProvider.SettingsColumns.AUTO_ADVANCE,
2043                Integer.toString(UIProvider.AutoAdvance.NEWER))
2044        .add(UIProvider.SettingsColumns.MESSAGE_TEXT_SIZE,
2045                Integer.toString(UIProvider.MessageTextSize.NORMAL))
2046        .add(UIProvider.SettingsColumns.SNAP_HEADERS,
2047                Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
2048        .add(UIProvider.SettingsColumns.REPLY_BEHAVIOR,
2049                Integer.toString(UIProvider.DefaultReplyBehavior.REPLY))
2050        .add(UIProvider.SettingsColumns.HIDE_CHECKBOXES, "0")
2051        .add(UIProvider.SettingsColumns.CONFIRM_DELETE, "0")
2052        .add(UIProvider.SettingsColumns.CONFIRM_ARCHIVE, "0")
2053        .add(UIProvider.SettingsColumns.CONFIRM_SEND, "0")
2054        .build();
2055
2056    /**
2057     * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
2058     */
2059    private static final ProjectionMap sAttachmentMap = ProjectionMap.builder()
2060        .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
2061        .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
2062        .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
2063        .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
2064        .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
2065        .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
2066        .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, AttachmentColumns.UI_DOWNLOADED_SIZE)
2067        .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
2068        .build();
2069
2070    /**
2071     * Generate the SELECT clause using a specified mapping and the original UI projection
2072     * @param map the ProjectionMap to use for this projection
2073     * @param projection the projection as sent by UnifiedEmail
2074     * @param values ContentValues to be used if the ProjectionMap entry is null
2075     * @return a StringBuilder containing the SELECT expression for a SQLite query
2076     */
2077    private StringBuilder genSelect(ProjectionMap map, String[] projection) {
2078        return genSelect(map, projection, EMPTY_CONTENT_VALUES);
2079    }
2080
2081    private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) {
2082        StringBuilder sb = new StringBuilder("SELECT ");
2083        boolean first = true;
2084        for (String column: projection) {
2085            if (first) {
2086                first = false;
2087            } else {
2088                sb.append(',');
2089            }
2090            String val = null;
2091            // First look at values; this is an override of default behavior
2092            if (values.containsKey(column)) {
2093                val = "'" + values.getAsString(column) + "' AS " + column;
2094            } else {
2095                // Now, get the standard value for the column from our projection map
2096                val = map.get(column);
2097                // If we don't have the column, return "NULL AS <column>", and warn
2098                if (val == null) {
2099                    Log.w(TAG, "UIProvider column not found, returning NULL: " + column);
2100                    val = "NULL AS " + column;
2101                }
2102            }
2103            sb.append(val);
2104        }
2105        return sb;
2106    }
2107
2108    /**
2109     * Convenience method to create a Uri string given the "type" of query; we append the type
2110     * of the query and the id column name (_id)
2111     *
2112     * @param type the "type" of the query, as defined by our UriMatcher definitions
2113     * @return a Uri string
2114     */
2115    private static String uriWithId(String type) {
2116        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id";
2117    }
2118
2119    /**
2120     * Convenience method to create a Uri string given the "type" of query and the table name to
2121     * which it applies; we append the type of the query and the fully qualified (FQ) id column
2122     * (i.e. including the table name); we need this for join queries where _id would otherwise
2123     * be ambiguous
2124     *
2125     * @param type the "type" of the query, as defined by our UriMatcher definitions
2126     * @param tableName the name of the table whose _id is referred to
2127     * @return a Uri string
2128     */
2129    private static String uriWithFQId(String type, String tableName) {
2130        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
2131    }
2132
2133    /**
2134     * Generate the "view message" SQLite query, given a projection from UnifiedEmail
2135     *
2136     * @param uiProjection as passed from UnifiedEmail
2137     * @return the SQLite query to be executed on the EmailProvider database
2138     */
2139    private String genQueryViewMessage(String[] uiProjection, String id) {
2140        Context context = getContext();
2141        long messageId = Long.parseLong(id);
2142        Message msg = Message.restoreMessageWithId(context, messageId);
2143        if (msg != null && (msg.mFlagLoaded == Message.FLAG_LOADED_PARTIAL)) {
2144            EmailServiceProxy service =
2145                    EmailServiceUtils.getServiceForAccount(context, null, msg.mAccountKey);
2146            try {
2147                service.loadMore(messageId);
2148            } catch (RemoteException e) {
2149                // Nothing to do
2150            }
2151        }
2152        StringBuilder sb = genSelect(sMessageViewMap, uiProjection);
2153        sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " +
2154                Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " +
2155                Message.TABLE_NAME + "." + Message.RECORD_ID + "=?");
2156        return sb.toString();
2157    }
2158
2159    /**
2160     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
2161     *
2162     * @param uiProjection as passed from UnifiedEmail
2163     * @return the SQLite query to be executed on the EmailProvider database
2164     */
2165    private String genQueryMailboxMessages(String[] uiProjection) {
2166        StringBuilder sb = genSelect(sMessageListMap, uiProjection);
2167        // Make constant
2168        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " +
2169                MessageColumns.TIMESTAMP + " DESC");
2170        return sb.toString();
2171    }
2172
2173    /**
2174     * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
2175     *
2176     * @param uiProjection as passed from UnifiedEmail
2177     * @return the SQLite query to be executed on the EmailProvider database
2178     */
2179    private String genQueryAccountMailboxes(String[] uiProjection) {
2180        StringBuilder sb = genSelect(sFolderListMap, uiProjection);
2181        // Make constant
2182        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
2183                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
2184                " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
2185        sb.append(MAILBOX_ORDER_BY);
2186        return sb.toString();
2187    }
2188
2189    /**
2190     * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
2191     *
2192     * @param uiProjection as passed from UnifiedEmail
2193     * @return the SQLite query to be executed on the EmailProvider database
2194     */
2195    private String genQueryMailbox(String[] uiProjection, String id) {
2196        long mailboxId = Long.parseLong(id);
2197        ContentValues values = EMPTY_CONTENT_VALUES;
2198        if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
2199            // This is the current search mailbox; use the total count
2200            values = new ContentValues();
2201            values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount);
2202            // "load more" is valid for search results
2203            values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2204                    uiUriString("uiloadmore", mailboxId));
2205        } else {
2206            Context context = getContext();
2207            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
2208            String protocol = Account.getProtocol(context, mailbox.mAccountKey);
2209            // "load more" is valid for IMAP/POP3
2210            if (HostAuth.SCHEME_IMAP.equals(protocol) || HostAuth.SCHEME_POP3.equals(protocol)) {
2211                values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2212                        uiUriString("uiloadmore", mailboxId));
2213            }
2214        }
2215        StringBuilder sb = genSelect(sFolderListMap, uiProjection, values);
2216        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?");
2217        return sb.toString();
2218    }
2219
2220    private static final long IMAP_CAPABILITIES =
2221            AccountCapabilities.SYNCABLE_FOLDERS |
2222            AccountCapabilities.FOLDER_SERVER_SEARCH |
2223            AccountCapabilities.UNDO;
2224
2225    private static final long POP3_CAPABILITIES = 0;
2226
2227    private static final long EAS_12_CAPABILITIES =
2228            AccountCapabilities.SYNCABLE_FOLDERS |
2229            AccountCapabilities.FOLDER_SERVER_SEARCH |
2230            AccountCapabilities.SANITIZED_HTML |
2231            AccountCapabilities.SMART_REPLY |
2232            AccountCapabilities.SERVER_SEARCH |
2233            AccountCapabilities.UNDO;
2234
2235    private static final long EAS_2_CAPABILITIES =
2236            AccountCapabilities.SYNCABLE_FOLDERS |
2237            AccountCapabilities.SANITIZED_HTML |
2238            AccountCapabilities.SMART_REPLY |
2239            AccountCapabilities.UNDO;
2240
2241    private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com");
2242
2243    private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
2244
2245    private static String getExternalUriString(String segment, String account) {
2246        return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
2247                .appendQueryParameter("account", account).build().toString();
2248    }
2249
2250    private static String getExternalUriStringEmail2(String segment, String account) {
2251        return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
2252                .appendQueryParameter("account", account).build().toString();
2253    }
2254
2255    /**
2256     * Generate a "single account" SQLite query, given a projection from UnifiedEmail
2257     *
2258     * @param uiProjection as passed from UnifiedEmail
2259     * @return the SQLite query to be executed on the EmailProvider database
2260     */
2261    // TODO: Get protocol specific stuff out of here (it should be in the account)
2262    private String genQueryAccount(String[] uiProjection, String id) {
2263        ContentValues values = new ContentValues();
2264        long accountId = Long.parseLong(id);
2265        String protocol = Account.getProtocol(getContext(), accountId);
2266        if (HostAuth.SCHEME_IMAP.equals(protocol)) {
2267            values.put(UIProvider.AccountColumns.CAPABILITIES, IMAP_CAPABILITIES);
2268        } else if (HostAuth.SCHEME_POP3.equals(protocol)) {
2269            values.put(UIProvider.AccountColumns.CAPABILITIES, POP3_CAPABILITIES);
2270        } else {
2271            Account account = Account.restoreAccountWithId(getContext(), accountId);
2272            String easVersion = account.mProtocolVersion;
2273            Double easVersionDouble = 2.5D;
2274            if (easVersion != null) {
2275                try {
2276                    easVersionDouble = Double.parseDouble(easVersion);
2277                } catch (NumberFormatException e) {
2278                    // Stick with 2.5
2279                }
2280            }
2281            if (easVersionDouble >= 12.0D) {
2282                values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_12_CAPABILITIES);
2283            } else {
2284                values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_2_CAPABILITIES);
2285            }
2286        }
2287        values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
2288                getExternalUriString("settings", id));
2289        values.put(UIProvider.AccountColumns.COMPOSE_URI,
2290                getExternalUriStringEmail2("compose", id));
2291        values.put(UIProvider.AccountColumns.MIME_TYPE, "application/email-ls");
2292        StringBuilder sb = genSelect(sAccountListMap, uiProjection, values);
2293        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
2294        return sb.toString();
2295    }
2296
2297    /**
2298     * Generate an "account settings" SQLite query, given a projection from UnifiedEmail
2299     *
2300     * @param uiProjection as passed from UnifiedEmail
2301     * @return the SQLite query to be executed on the EmailProvider database
2302     */
2303    private String genQuerySettings(String[] uiProjection, String id) {
2304        ContentValues values = new ContentValues();
2305        long accountId = Long.parseLong(id);
2306        long mailboxId = Mailbox.findMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
2307        if (mailboxId != Mailbox.NO_MAILBOX) {
2308            values.put(UIProvider.SettingsColumns.DEFAULT_INBOX,
2309                    uiUriString("uifolder", mailboxId));
2310        }
2311        StringBuilder sb = genSelect(sAccountSettingsMap, uiProjection, values);
2312        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
2313        return sb.toString();
2314    }
2315
2316    private Cursor uiAccounts(String[] uiProjection) {
2317        Context context = getContext();
2318        SQLiteDatabase db = getDatabase(context);
2319        Cursor accountIdCursor =
2320                db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
2321        MatrixCursor mc = new MatrixCursor(uiProjection, accountIdCursor.getCount());
2322        Object[] values = new Object[uiProjection.length];
2323        try {
2324            while (accountIdCursor.moveToNext()) {
2325                String id = accountIdCursor.getString(0);
2326                Cursor accountCursor =
2327                        db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
2328                if (accountCursor.moveToNext()) {
2329                    for (int i = 0; i < uiProjection.length; i++) {
2330                        values[i] = accountCursor.getString(i);
2331                    }
2332                    mc.addRow(values);
2333                }
2334                accountCursor.close();
2335            }
2336        } finally {
2337            accountIdCursor.close();
2338        }
2339        return mc;
2340    }
2341
2342    /**
2343     * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
2344     *
2345     * @param uiProjection as passed from UnifiedEmail
2346     * @return the SQLite query to be executed on the EmailProvider database
2347     */
2348    private String genQueryAttachments(String[] uiProjection) {
2349        StringBuilder sb = genSelect(sAttachmentMap, uiProjection);
2350        sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY +
2351                " =? ");
2352        return sb.toString();
2353    }
2354
2355    /**
2356     * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
2357     *
2358     * @param uiProjection as passed from UnifiedEmail
2359     * @return the SQLite query to be executed on the EmailProvider database
2360     */
2361    private String genQueryAttachment(String[] uiProjection) {
2362        StringBuilder sb = genSelect(sAttachmentMap, uiProjection);
2363        sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? ");
2364        return sb.toString();
2365    }
2366
2367    /**
2368     * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
2369     *
2370     * @param uiProjection as passed from UnifiedEmail
2371     * @return the SQLite query to be executed on the EmailProvider database
2372     */
2373    private String genQuerySubfolders(String[] uiProjection) {
2374        StringBuilder sb = genSelect(sFolderListMap, uiProjection);
2375        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
2376                " =? ORDER BY ");
2377        sb.append(MAILBOX_ORDER_BY);
2378        return sb.toString();
2379    }
2380
2381    /**
2382     * Handle UnifiedEmail queries here (dispatched from query())
2383     *
2384     * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
2385     * @param uri the original uri passed in from UnifiedEmail
2386     * @param uiProjection the projection passed in from UnifiedEmail
2387     * @return the result Cursor
2388     */
2389    private Cursor uiQuery(int match, Uri uri, String[] uiProjection) {
2390        Context context = getContext();
2391        ContentResolver resolver = context.getContentResolver();
2392        SQLiteDatabase db = getDatabase(context);
2393        // Should we ever return null, or throw an exception??
2394        Cursor c = null;
2395        String id = uri.getPathSegments().get(1);
2396        Uri notifyUri = null;
2397        switch(match) {
2398            case UI_FOLDERS:
2399                c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id});
2400                break;
2401            case UI_SUBFOLDERS:
2402                c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id});
2403                break;
2404            case UI_MESSAGES:
2405                c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id});
2406                notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
2407                break;
2408            case UI_MESSAGE:
2409                c = db.rawQuery(genQueryViewMessage(uiProjection, id), new String[] {id});
2410                break;
2411            case UI_ATTACHMENTS:
2412                c = db.rawQuery(genQueryAttachments(uiProjection), new String[] {id});
2413                notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
2414                break;
2415            case UI_ATTACHMENT:
2416                c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id});
2417                notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
2418                break;
2419            case UI_FOLDER:
2420                c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id});
2421                notifyUri = UIPROVIDER_MAILBOX_NOTIFIER.buildUpon().appendPath(id).build();
2422                break;
2423            case UI_ACCOUNT:
2424                c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
2425                notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
2426                break;
2427            case UI_SETTINGS:
2428                c = db.rawQuery(genQuerySettings(uiProjection, id), new String[] {id});
2429                notifyUri = UIPROVIDER_SETTINGS_NOTIFIER.buildUpon().appendPath(id).build();
2430                break;
2431        }
2432        if (notifyUri != null) {
2433            c.setNotificationUri(resolver, notifyUri);
2434        }
2435        return c;
2436    }
2437
2438    /**
2439     * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need
2440     * a few of the fields
2441     * @param uiAtt the UIProvider attachment to convert
2442     * @return the EmailProvider attachment
2443     */
2444    private Attachment convertUiAttachmentToAttachment(
2445            com.android.mail.providers.Attachment uiAtt) {
2446        Attachment att = new Attachment();
2447        att.mContentUri = uiAtt.contentUri;
2448        att.mFileName = uiAtt.name;
2449        att.mMimeType = uiAtt.mimeType;
2450        att.mSize = uiAtt.size;
2451        return att;
2452    }
2453
2454    /**
2455     * Create a mailbox given the account and mailboxType.
2456     */
2457    private Mailbox createMailbox(long accountId, int mailboxType) {
2458        Context context = getContext();
2459        int resId = -1;
2460        switch (mailboxType) {
2461            case Mailbox.TYPE_INBOX:
2462                resId = R.string.mailbox_name_server_inbox;
2463                break;
2464            case Mailbox.TYPE_OUTBOX:
2465                resId = R.string.mailbox_name_server_outbox;
2466                break;
2467            case Mailbox.TYPE_DRAFTS:
2468                resId = R.string.mailbox_name_server_drafts;
2469                break;
2470            case Mailbox.TYPE_TRASH:
2471                resId = R.string.mailbox_name_server_trash;
2472                break;
2473            case Mailbox.TYPE_SENT:
2474                resId = R.string.mailbox_name_server_sent;
2475                break;
2476            case Mailbox.TYPE_JUNK:
2477                resId = R.string.mailbox_name_server_junk;
2478                break;
2479            default:
2480                throw new IllegalArgumentException("Illegal mailbox type");
2481        }
2482        Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId);
2483        Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId));
2484        // Make sure drafts and save will show up in recents...
2485        // If these already exist (from old Email app), they will have touch times
2486        switch (mailboxType) {
2487            case Mailbox.TYPE_DRAFTS:
2488                box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
2489                break;
2490            case Mailbox.TYPE_SENT:
2491                box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
2492                break;
2493        }
2494        box.save(context);
2495        return box;
2496    }
2497
2498    /**
2499     * Given an account name and a mailbox type, return that mailbox, creating it if necessary
2500     * @param accountName the account name to use
2501     * @param mailboxType the type of mailbox we're trying to find
2502     * @return the mailbox of the given type for the account in the uri, or null if not found
2503     */
2504    private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) {
2505        long id = Long.parseLong(accountId);
2506        Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType);
2507        if (mailbox == null) {
2508            mailbox = createMailbox(id, mailboxType);
2509        }
2510        return mailbox;
2511    }
2512
2513    private Message getMessageFromPathSegments(List<String> pathSegments) {
2514        Message msg = null;
2515        if (pathSegments.size() > 2) {
2516            msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2)));
2517        }
2518        if (msg == null) {
2519            msg = new Message();
2520        }
2521        return msg;
2522    }
2523    /**
2524     * Given a mailbox and the content values for a message, create/save the message in the mailbox
2525     * @param mailbox the mailbox to use
2526     * @param values the content values that represent message fields
2527     * @return the uri of the newly created message
2528     */
2529    private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) {
2530        Context context = getContext();
2531        // Fill in the message
2532        Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
2533        if (account == null) return null;
2534        msg.mFrom = account.mEmailAddress;
2535        msg.mTimeStamp = System.currentTimeMillis();
2536        msg.mTo = values.getAsString(UIProvider.MessageColumns.TO);
2537        msg.mCc = values.getAsString(UIProvider.MessageColumns.CC);
2538        msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC);
2539        msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT);
2540        msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT);
2541        msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML);
2542        msg.mMailboxKey = mailbox.mId;
2543        msg.mAccountKey = mailbox.mAccountKey;
2544        msg.mDisplayName = msg.mTo;
2545        msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
2546        // Get attachments from the ContentValues
2547        ArrayList<com.android.mail.providers.Attachment> uiAtts =
2548                com.android.mail.providers.Attachment.getAttachmentsFromJoinedAttachmentInfo(
2549                        values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS));
2550        ArrayList<Attachment> atts = new ArrayList<Attachment>();
2551        for (com.android.mail.providers.Attachment uiAtt: uiAtts) {
2552            // Convert to our attachments and add to the list; everything else should "just work"
2553            atts.add(convertUiAttachmentToAttachment(uiAtt));
2554        }
2555        if (!atts.isEmpty()) {
2556            msg.mAttachments = atts;
2557        }
2558        // Save it or update it...
2559        if (!msg.isSaved()) {
2560            msg.save(context);
2561        } else {
2562            // This is tricky due to how messages/attachments are saved; rather than putz with
2563            // what's changed, we'll delete/re-add them
2564            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
2565            // Delete all existing attachments
2566            ops.add(ContentProviderOperation.newDelete(
2567                    ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId))
2568                    .build());
2569            // Delete the body
2570            ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI)
2571                    .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)})
2572                    .build());
2573            // Add the ops for the message, atts, and body
2574            msg.addSaveOps(ops);
2575            // Do it!
2576            try {
2577                applyBatch(ops);
2578            } catch (OperationApplicationException e) {
2579            }
2580        }
2581        if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
2582            EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context,
2583                    mServiceCallback, mailbox.mAccountKey);
2584            try {
2585                service.startSync(mailbox.mId, true);
2586            } catch (RemoteException e) {
2587            }
2588        }
2589        return uiUri("uimessage", msg.mId);
2590    }
2591
2592    /**
2593     * Create and send the message via the account indicated in the uri
2594     * @param uri the incoming uri
2595     * @param values the content values that represent message fields
2596     * @return the uri of the created message
2597     */
2598    private Uri uiSendMail(Uri uri, ContentValues values) {
2599        List<String> pathSegments = uri.getPathSegments();
2600        Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX);
2601        if (mailbox == null) return null;
2602        Message msg = getMessageFromPathSegments(pathSegments);
2603        try {
2604            return uiSaveMessage(msg, mailbox, values);
2605        } finally {
2606            // Kick observers
2607            getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null);
2608        }
2609    }
2610
2611    /**
2612     * Create a message and save it to the drafts folder of the account indicated in the uri
2613     * @param uri the incoming uri
2614     * @param values the content values that represent message fields
2615     * @return the uri of the created message
2616     */
2617    private Uri uiSaveDraft(Uri uri, ContentValues values) {
2618        List<String> pathSegments = uri.getPathSegments();
2619        Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS);
2620        if (mailbox == null) return null;
2621        Message msg = getMessageFromPathSegments(pathSegments);
2622        return uiSaveMessage(msg, mailbox, values);
2623    }
2624
2625    private int uiUpdateDraft(Uri uri, ContentValues values) {
2626        Context context = getContext();
2627        Message msg = Message.restoreMessageWithId(context,
2628                Long.parseLong(uri.getPathSegments().get(1)));
2629        if (msg == null) return 0;
2630        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
2631        if (mailbox == null) return 0;
2632        uiSaveMessage(msg, mailbox, values);
2633        return 1;
2634    }
2635
2636    private int uiSendDraft(Uri uri, ContentValues values) {
2637        Context context = getContext();
2638        Message msg = Message.restoreMessageWithId(context,
2639                Long.parseLong(uri.getPathSegments().get(1)));
2640        if (msg == null) return 0;
2641        long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX);
2642        if (mailboxId == Mailbox.NO_MAILBOX) return 0;
2643        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
2644        if (mailbox == null) return 0;
2645        uiSaveMessage(msg, mailbox, values);
2646        // Kick observers
2647        context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null);
2648        return 1;
2649    }
2650
2651    private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) {
2652        if (value instanceof Integer) {
2653            Integer intValue = (Integer)value;
2654            values.put(columnName, intValue);
2655        } else if (value instanceof Boolean) {
2656            Boolean boolValue = (Boolean)value;
2657            values.put(columnName, boolValue ? 1 : 0);
2658        } else if (value instanceof Long) {
2659            Long longValue = (Long)value;
2660            values.put(columnName, longValue);
2661        }
2662    }
2663
2664    private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
2665        Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
2666        if (stateValue != null) {
2667            // This is a command from UIProvider
2668            long attachmentId = Long.parseLong(uri.getLastPathSegment());
2669            Context context = getContext();
2670            Attachment attachment =
2671                    Attachment.restoreAttachmentWithId(context, attachmentId);
2672            if (attachment == null) {
2673                // Went away; ah, well...
2674                return 0;
2675            }
2676            ContentValues values = new ContentValues();
2677            switch (stateValue.intValue()) {
2678                case UIProvider.AttachmentState.NOT_SAVED:
2679                    // Set state, try to cancel request
2680                    values.put(AttachmentColumns.UI_STATE, stateValue);
2681                    values.put(AttachmentColumns.FLAGS,
2682                            attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
2683                    attachment.update(context, values);
2684                    return 1;
2685                case UIProvider.AttachmentState.DOWNLOADING:
2686                    // Set state and destination; request download
2687                    values.put(AttachmentColumns.UI_STATE, stateValue);
2688                    Integer destinationValue =
2689                        uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
2690                    values.put(AttachmentColumns.UI_DESTINATION,
2691                            destinationValue == null ? 0 : destinationValue);
2692                    values.put(AttachmentColumns.FLAGS,
2693                            attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
2694                    attachment.update(context, values);
2695                    return 1;
2696            }
2697        }
2698        return 0;
2699    }
2700
2701    private ContentValues convertUiMessageValues(ContentValues values) {
2702        ContentValues ourValues = new ContentValues();
2703        for (String columnName: values.keySet()) {
2704            Object val = values.get(columnName);
2705            if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
2706                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
2707            } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
2708                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
2709            } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
2710                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
2711            } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) {
2712                // Convert from folder list uri to mailbox key
2713                Uri uri = Uri.parse((String)val);
2714                Long mailboxId = Long.parseLong(uri.getLastPathSegment());
2715                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
2716            } else {
2717                throw new IllegalArgumentException("Can't update " + columnName + " in message");
2718            }
2719        }
2720        return ourValues;
2721    }
2722
2723    private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) {
2724        String idString = uri.getLastPathSegment();
2725        try {
2726            long id = Long.parseLong(idString);
2727            Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id);
2728            if (asProvider) {
2729                ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
2730            }
2731            return ourUri;
2732        } catch (NumberFormatException e) {
2733            return null;
2734        }
2735    }
2736
2737    private Message getMessageFromLastSegment(Uri uri) {
2738        long messageId = Long.parseLong(uri.getLastPathSegment());
2739        return Message.restoreMessageWithId(getContext(), messageId);
2740    }
2741
2742    /**
2743     * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
2744     * clear out the undo list and start over
2745     * @param uri the uri we're working on
2746     * @param op the ContentProviderOperation to perform upon undo
2747     */
2748    private void addToSequence(Uri uri, ContentProviderOperation op) {
2749        String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
2750        if (sequenceString != null) {
2751            int sequence = Integer.parseInt(sequenceString);
2752            if (sequence > mLastSequence) {
2753                // Reset sequence
2754                mLastSequenceOps.clear();
2755                mLastSequence = sequence;
2756            }
2757            // TODO: Need something to indicate a change isn't ready (undoable)
2758            mLastSequenceOps.add(op);
2759        }
2760    }
2761
2762    private int uiUpdateMessage(Uri uri, ContentValues values) {
2763        Uri ourUri = convertToEmailProviderUri(uri, true);
2764        if (ourUri == null) return 0;
2765        ContentValues ourValues = convertUiMessageValues(values);
2766        Message msg = getMessageFromLastSegment(uri);
2767        if (msg == null) return 0;
2768        ContentValues undoValues = new ContentValues();
2769        for (String columnName: ourValues.keySet()) {
2770            if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
2771                undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
2772            } else if (columnName.equals(MessageColumns.FLAG_READ)) {
2773                undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
2774            } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
2775                undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
2776            }
2777        }
2778        ContentProviderOperation op =
2779                ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false))
2780                        .withValues(undoValues)
2781                        .build();
2782        addToSequence(uri, op);
2783        return update(ourUri, ourValues, null, null);
2784    }
2785
2786    private int uiDeleteMessage(Uri uri) {
2787        Context context = getContext();
2788        Message msg = getMessageFromLastSegment(uri);
2789        if (msg == null) return 0;
2790        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
2791        if (mailbox == null) return 0;
2792        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
2793            // We actually delete these, including attachments
2794            AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
2795            return context.getContentResolver().delete(
2796                    ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null);
2797        }
2798        Mailbox trashMailbox =
2799                Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
2800        if (trashMailbox == null) return 0;
2801        ContentValues values = new ContentValues();
2802        values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
2803        return uiUpdateMessage(uri, values);
2804    }
2805
2806    private Cursor uiUndo(Uri uri, String[] projection) {
2807        // First see if we have any operations saved
2808        // TODO: Make sure seq matches
2809        if (!mLastSequenceOps.isEmpty()) {
2810            try {
2811                // TODO Always use this projection?  Or what's passed in?
2812                // Not sure if UI wants it, but I'm making a cursor of convo uri's
2813                MatrixCursor c = new MatrixCursor(
2814                        new String[] {UIProvider.ConversationColumns.URI},
2815                        mLastSequenceOps.size());
2816                for (ContentProviderOperation op: mLastSequenceOps) {
2817                    c.addRow(new String[] {op.getUri().toString()});
2818                }
2819                // Just apply the batch and we're done!
2820                applyBatch(mLastSequenceOps);
2821                // But clear the operations
2822                mLastSequenceOps.clear();
2823                // Tell the UI there are changes
2824                getContext().getContentResolver().notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER,
2825                        null);
2826                Log.d(TAG, "[Notify UI: Undo]");
2827                return c;
2828            } catch (OperationApplicationException e) {
2829            }
2830        }
2831        return new MatrixCursor(projection, 0);
2832    }
2833
2834    private void notifyUIConversation(Uri uri) {
2835        String id = uri.getLastPathSegment();
2836        Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
2837        if (msg != null) {
2838            notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(msg.mMailboxKey));
2839        }
2840    }
2841
2842    private void notifyUIConversationMailbox(long id) {
2843        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
2844    }
2845
2846    private void notifyUI(Uri uri, String id) {
2847        Uri notifyUri = uri.buildUpon().appendPath(id).build();
2848        getContext().getContentResolver().notifyChange(notifyUri, null);
2849        // Temporary
2850        Log.d(TAG, "[Notify UI: " + notifyUri + "]");
2851    }
2852
2853    private void notifyUI(Uri uri, long id) {
2854        notifyUI(uri, Long.toString(id));
2855    }
2856
2857    /**
2858     * Support for services and service notifications
2859     */
2860
2861    private final IEmailServiceCallback.Stub mServiceCallback =
2862            new IEmailServiceCallback.Stub() {
2863
2864        @Override
2865        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
2866                throws RemoteException {
2867        }
2868
2869        @Override
2870        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
2871                throws RemoteException {
2872            // We'll get callbacks here from the services, which we'll pass back to the UI
2873            Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId);
2874            EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null);
2875        }
2876
2877        @Override
2878        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
2879                int progress) throws RemoteException {
2880        }
2881
2882        @Override
2883        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
2884                throws RemoteException {
2885        }
2886
2887        @Override
2888        public void loadMessageStatus(long messageId, int statusCode, int progress)
2889                throws RemoteException {
2890        }
2891    };
2892
2893    private Cursor uiFolderRefresh(Uri uri) {
2894        Context context = getContext();
2895        String idString = uri.getLastPathSegment();
2896        long id = Long.parseLong(idString);
2897        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id);
2898        if (mailbox == null) return null;
2899        EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context,
2900                mServiceCallback, mailbox.mAccountKey);
2901        try {
2902            service.startSync(id, true);
2903        } catch (RemoteException e) {
2904        }
2905        return null;
2906    }
2907
2908    //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
2909    public static final int VISIBLE_LIMIT_INCREMENT = 10;
2910    //Number of additional messages to load when a user selects "Load more..." in a search
2911    public static final int SEARCH_MORE_INCREMENT = 10;
2912
2913    private Cursor uiFolderLoadMore(Uri uri) {
2914        Context context = getContext();
2915        String idString = uri.getLastPathSegment();
2916        long id = Long.parseLong(idString);
2917        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id);
2918        if (mailbox == null) return null;
2919        if (mailbox.mType == Mailbox.TYPE_SEARCH) {
2920            // Ask for 10 more messages
2921            mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
2922            runSearchQuery(context, mailbox.mAccountKey, id);
2923        } else {
2924            ContentValues values = new ContentValues();
2925            values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
2926            values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT);
2927            Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id);
2928            // Increase the limit
2929            context.getContentResolver().update(mailboxUri, values, null, null);
2930            // And order a refresh
2931            uiFolderRefresh(uri);
2932        }
2933        return null;
2934    }
2935
2936    private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
2937    private SearchParams mSearchParams;
2938
2939    /**
2940     * Returns the search mailbox for the specified account, creating one if necessary
2941     * @return the search mailbox for the passed in account
2942     */
2943    private Mailbox getSearchMailbox(long accountId) {
2944        Context context = getContext();
2945        Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
2946        if (m == null) {
2947            m = new Mailbox();
2948            m.mAccountKey = accountId;
2949            m.mServerId = SEARCH_MAILBOX_SERVER_ID;
2950            m.mFlagVisible = false;
2951            m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
2952            m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
2953            m.mType = Mailbox.TYPE_SEARCH;
2954            m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
2955            m.mParentKey = Mailbox.NO_MAILBOX;
2956            m.save(context);
2957        }
2958        return m;
2959    }
2960
2961    private void runSearchQuery(final Context context, final long accountId,
2962            final long searchMailboxId) {
2963        // Start the search running in the background
2964        new Thread(new Runnable() {
2965            @Override
2966            public void run() {
2967                 try {
2968                    EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context,
2969                            mServiceCallback, accountId);
2970                    if (service != null) {
2971                        try {
2972                            // Save away the total count
2973                            mSearchParams.mTotalCount = service.searchMessages(accountId,
2974                                    mSearchParams, searchMailboxId);
2975                            Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount);
2976                            notifyUI(UIPROVIDER_MAILBOX_NOTIFIER, searchMailboxId);
2977                        } catch (RemoteException e) {
2978                            Log.e("searchMessages", "RemoteException", e);
2979                        }
2980                    }
2981                } finally {
2982                }
2983            }}).start();
2984
2985    }
2986
2987    // TODO: Handle searching for more...
2988    private Cursor uiSearch(Uri uri, String[] projection) {
2989        final long accountId = Long.parseLong(uri.getLastPathSegment());
2990
2991        // TODO: Check the actual mailbox
2992        Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
2993        if (inbox == null) return null;
2994
2995        String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
2996        if (filter == null) {
2997            throw new IllegalArgumentException("No query parameter in search query");
2998        }
2999
3000        // Find/create our search mailbox
3001        Mailbox searchMailbox = getSearchMailbox(accountId);
3002        final long searchMailboxId = searchMailbox.mId;
3003
3004        mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
3005
3006        final Context context = getContext();
3007        if (mSearchParams.mOffset == 0) {
3008            // Delete existing contents of search mailbox
3009            ContentResolver resolver = context.getContentResolver();
3010            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
3011                    null);
3012            ContentValues cv = new ContentValues();
3013            // For now, use the actual query as the name of the mailbox
3014            cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
3015            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
3016                    cv, null, null);
3017        }
3018
3019        // Start the search running in the background
3020        runSearchQuery(context, accountId, searchMailboxId);
3021
3022        // This will look just like a "normal" folder
3023        return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
3024                searchMailbox.mId), projection);
3025    }
3026
3027    private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
3028    private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
3029        MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
3030        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
3031    private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?";
3032
3033    /**
3034     * Delete an account and clean it up
3035     */
3036    private int uiDeleteAccount(Uri uri) {
3037        Context context = getContext();
3038        long accountId = Long.parseLong(uri.getLastPathSegment());
3039        try {
3040            // Get the account URI.
3041            final Account account = Account.restoreAccountWithId(context, accountId);
3042            if (account == null) {
3043                return 0; // Already deleted?
3044            }
3045
3046            deleteAccountData(context, accountId);
3047
3048            // Now delete the account itself
3049            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
3050            context.getContentResolver().delete(uri, null, null);
3051
3052            // Clean up
3053            AccountBackupRestore.backup(context);
3054            SecurityPolicy.getInstance(context).reducePolicies();
3055            Email.setServicesEnabledSync(context);
3056            return 1;
3057        } catch (Exception e) {
3058            Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
3059        }
3060        return 0;
3061    }
3062
3063    private int uiDeleteAccountData(Uri uri) {
3064        Context context = getContext();
3065        long accountId = Long.parseLong(uri.getLastPathSegment());
3066        // Get the account URI.
3067        final Account account = Account.restoreAccountWithId(context, accountId);
3068        if (account == null) {
3069            return 0; // Already deleted?
3070        }
3071        deleteAccountData(context, accountId);
3072        return 1;
3073    }
3074
3075    private void deleteAccountData(Context context, long accountId) {
3076        // Delete synced attachments
3077        AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
3078
3079        // Delete synced email, leaving only an empty inbox.  We do this in two phases:
3080        // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
3081        // 2. Delete all remaining messages (which will be the inbox messages)
3082        ContentResolver resolver = context.getContentResolver();
3083        String[] accountIdArgs = new String[] { Long.toString(accountId) };
3084        resolver.delete(Mailbox.CONTENT_URI,
3085                MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
3086                accountIdArgs);
3087        resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
3088
3089        // Delete sync keys on remaining items
3090        ContentValues cv = new ContentValues();
3091        cv.putNull(Account.SYNC_KEY);
3092        resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
3093        cv.clear();
3094        cv.putNull(Mailbox.SYNC_KEY);
3095        resolver.update(Mailbox.CONTENT_URI, cv,
3096                MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
3097
3098        // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
3099        IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId);
3100        if (service != null) {
3101            try {
3102                service.deleteAccountPIMData(accountId);
3103            } catch (RemoteException e) {
3104                // Can't do anything about this
3105            }
3106        }
3107    }
3108}
3109