EmailProvider.java revision 6f627965d00bbe137f848cf6593556ce66a53372
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                // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
978                // or DELETED_MESSAGE; see the comment below for details
979                case UPDATED_MESSAGE:
980                case DELETED_MESSAGE:
981                case MESSAGE:
982                case BODY:
983                case ATTACHMENT:
984                case MAILBOX:
985                case ACCOUNT:
986                case HOSTAUTH:
987                case POLICY:
988                case QUICK_RESPONSE:
989                    longId = db.insert(TABLE_NAMES[table], "foo", values);
990                    resultUri = ContentUris.withAppendedId(uri, longId);
991                    switch(match) {
992                        case MESSAGE:
993                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
994                                notifyUIConversationMailbox(values.getAsLong(Message.MAILBOX_KEY));
995                            }
996                            break;
997                        case MAILBOX:
998                            if (values.containsKey(MailboxColumns.TYPE)) {
999                                // Only cache special mailbox types
1000                                int type = values.getAsInteger(MailboxColumns.TYPE);
1001                                if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX &&
1002                                        type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT &&
1003                                        type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) {
1004                                    break;
1005                                }
1006                            }
1007                            //$FALL-THROUGH$
1008                        case ACCOUNT:
1009                        case HOSTAUTH:
1010                        case POLICY:
1011                            // Cache new account, host auth, policy, and some mailbox rows
1012                            Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null);
1013                            if (c != null) {
1014                                if (match == MAILBOX) {
1015                                    addToMailboxTypeMap(c);
1016                                } else if (match == ACCOUNT) {
1017                                    getOrCreateAccountMailboxTypeMap(longId);
1018                                }
1019                                c.close();
1020                            }
1021                            break;
1022                    }
1023                    // Clients shouldn't normally be adding rows to these tables, as they are
1024                    // maintained by triggers.  However, we need to be able to do this for unit
1025                    // testing, so we allow the insert and then throw the same exception that we
1026                    // would if this weren't allowed.
1027                    if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) {
1028                        throw new IllegalArgumentException("Unknown URL " + uri);
1029                    }
1030                    if (match == ATTACHMENT) {
1031                        int flags = 0;
1032                        if (values.containsKey(Attachment.FLAGS)) {
1033                            flags = values.getAsInteger(Attachment.FLAGS);
1034                        }
1035                        // Report all new attachments to the download service
1036                        mAttachmentService.attachmentChanged(getContext(), longId, flags);
1037                    }
1038                    break;
1039                case MAILBOX_ID:
1040                    // This implies adding a message to a mailbox
1041                    // Hmm, a problem here is that we can't link the account as well, so it must be
1042                    // already in the values...
1043                    longId = Long.parseLong(uri.getPathSegments().get(1));
1044                    values.put(MessageColumns.MAILBOX_KEY, longId);
1045                    return insert(Message.CONTENT_URI, values); // Recurse
1046                case MESSAGE_ID:
1047                    // This implies adding an attachment to a message.
1048                    id = uri.getPathSegments().get(1);
1049                    longId = Long.parseLong(id);
1050                    values.put(AttachmentColumns.MESSAGE_KEY, longId);
1051                    return insert(Attachment.CONTENT_URI, values); // Recurse
1052                case ACCOUNT_ID:
1053                    // This implies adding a mailbox to an account.
1054                    longId = Long.parseLong(uri.getPathSegments().get(1));
1055                    values.put(MailboxColumns.ACCOUNT_KEY, longId);
1056                    return insert(Mailbox.CONTENT_URI, values); // Recurse
1057                case ATTACHMENTS_MESSAGE_ID:
1058                    longId = db.insert(TABLE_NAMES[table], "foo", values);
1059                    resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
1060                    break;
1061                default:
1062                    throw new IllegalArgumentException("Unknown URL " + uri);
1063            }
1064        } catch (SQLiteException e) {
1065            checkDatabases();
1066            throw e;
1067        }
1068
1069        // Notify all notifier cursors
1070        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
1071
1072        // Notify all existing cursors.
1073        resolver.notifyChange(EmailContent.CONTENT_URI, null);
1074        return resultUri;
1075    }
1076
1077    @Override
1078    public boolean onCreate() {
1079        checkDatabases();
1080        return false;
1081    }
1082
1083    /**
1084     * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
1085     * always be in sync (i.e. there are two database or NO databases).  This code will delete
1086     * any "orphan" database, so that both will be created together.  Note that an "orphan" database
1087     * will exist after either of the individual databases is deleted due to data corruption.
1088     */
1089    public void checkDatabases() {
1090        // Uncache the databases
1091        if (mDatabase != null) {
1092            mDatabase = null;
1093        }
1094        if (mBodyDatabase != null) {
1095            mBodyDatabase = null;
1096        }
1097        // Look for orphans, and delete as necessary; these must always be in sync
1098        File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
1099        File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
1100
1101        // TODO Make sure attachments are deleted
1102        if (databaseFile.exists() && !bodyFile.exists()) {
1103            Log.w(TAG, "Deleting orphaned EmailProvider database...");
1104            databaseFile.delete();
1105        } else if (bodyFile.exists() && !databaseFile.exists()) {
1106            Log.w(TAG, "Deleting orphaned EmailProviderBody database...");
1107            bodyFile.delete();
1108        }
1109    }
1110    @Override
1111    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1112            String sortOrder) {
1113        long time = 0L;
1114        if (Email.DEBUG) {
1115            time = System.nanoTime();
1116        }
1117        Cursor c = null;
1118        int match;
1119        try {
1120            match = findMatch(uri, "query");
1121        } catch (IllegalArgumentException e) {
1122            String uriString = uri.toString();
1123            // If we were passed an illegal uri, see if it ends in /-1
1124            // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
1125            if (uriString != null && uriString.endsWith("/-1")) {
1126                uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
1127                match = findMatch(uri, "query");
1128                switch (match) {
1129                    case BODY_ID:
1130                    case MESSAGE_ID:
1131                    case DELETED_MESSAGE_ID:
1132                    case UPDATED_MESSAGE_ID:
1133                    case ATTACHMENT_ID:
1134                    case MAILBOX_ID:
1135                    case ACCOUNT_ID:
1136                    case HOSTAUTH_ID:
1137                    case POLICY_ID:
1138                        return new MatrixCursor(projection, 0);
1139                }
1140            }
1141            throw e;
1142        }
1143        Context context = getContext();
1144        // See the comment at delete(), above
1145        SQLiteDatabase db = getDatabase(context);
1146        int table = match >> BASE_SHIFT;
1147        String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
1148        String id;
1149
1150        // Find the cache for this query's table (if any)
1151        ContentCache cache = null;
1152        String tableName = TABLE_NAMES[table];
1153        // We can only use the cache if there's no selection
1154        if (selection == null) {
1155            cache = mContentCaches[table];
1156        }
1157        if (cache == null) {
1158            ContentCache.notCacheable(uri, selection);
1159        }
1160
1161        try {
1162            switch (match) {
1163                // First, dispatch queries from UnfiedEmail
1164                case UI_SEARCH:
1165                    return uiSearch(uri, projection);
1166                case UI_ACCTS:
1167                    c = uiAccounts(projection);
1168                    return c;
1169                case UI_UNDO:
1170                    return uiUndo(uri, projection);
1171                case UI_SUBFOLDERS:
1172                case UI_FOLDERS:
1173                case UI_MESSAGES:
1174                case UI_MESSAGE:
1175                case UI_FOLDER:
1176                case UI_ACCOUNT:
1177                case UI_SETTINGS:
1178                case UI_ATTACHMENT:
1179                case UI_ATTACHMENTS:
1180                    // For now, we don't allow selection criteria within these queries
1181                    if (selection != null || selectionArgs != null) {
1182                        throw new IllegalArgumentException("UI queries can't have selection/args");
1183                    }
1184                    c = uiQuery(match, uri, projection);
1185                    return c;
1186                case UI_FOLDER_LOAD_MORE:
1187                    c = uiFolderLoadMore(uri);
1188                    return c;
1189                case UI_FOLDER_REFRESH:
1190                    c = uiFolderRefresh(uri);
1191                    return c;
1192                case MAILBOX_NOTIFICATION:
1193                    c = notificationQuery(uri);
1194                    return c;
1195                case MAILBOX_MOST_RECENT_MESSAGE:
1196                    c = mostRecentMessageQuery(uri);
1197                    return c;
1198                case ACCOUNT_DEFAULT_ID:
1199                    // Start with a snapshot of the cache
1200                    Map<String, Cursor> accountCache = mCacheAccount.getSnapshot();
1201                    long accountId = Account.NO_ACCOUNT;
1202                    // Find the account with "isDefault" set, or the lowest account ID otherwise.
1203                    // Note that the snapshot from the cached isn't guaranteed to be sorted in any
1204                    // way.
1205                    Collection<Cursor> accounts = accountCache.values();
1206                    for (Cursor accountCursor: accounts) {
1207                        // For now, at least, we can have zero count cursors (e.g. if someone looks
1208                        // up a non-existent id); we need to skip these
1209                        if (accountCursor.moveToFirst()) {
1210                            boolean isDefault =
1211                                accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1;
1212                            long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN);
1213                            // We'll remember this one if it's the default or the first one we see
1214                            if (isDefault) {
1215                                accountId = iterId;
1216                                break;
1217                            } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) {
1218                                accountId = iterId;
1219                            }
1220                        }
1221                    }
1222                    // Return a cursor with an id projection
1223                    MatrixCursor mc = new MatrixCursor(EmailContent.ID_PROJECTION);
1224                    mc.addRow(new Object[] {accountId});
1225                    c = mc;
1226                    break;
1227                case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE:
1228                    // Get accountId and type and find the mailbox in our map
1229                    List<String> pathSegments = uri.getPathSegments();
1230                    accountId = Long.parseLong(pathSegments.get(1));
1231                    int type = Integer.parseInt(pathSegments.get(2));
1232                    long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type);
1233                    // Return a cursor with an id projection
1234                    mc = new MatrixCursor(EmailContent.ID_PROJECTION);
1235                    mc.addRow(new Object[] {mailboxId});
1236                    c = mc;
1237                    break;
1238                case BODY:
1239                case MESSAGE:
1240                case UPDATED_MESSAGE:
1241                case DELETED_MESSAGE:
1242                case ATTACHMENT:
1243                case MAILBOX:
1244                case ACCOUNT:
1245                case HOSTAUTH:
1246                case POLICY:
1247                case QUICK_RESPONSE:
1248                    // Special-case "count of accounts"; it's common and we always know it
1249                    if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) &&
1250                            selection == null && limit.equals("1")) {
1251                        int accountCount = mMailboxTypeMap.size();
1252                        // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this
1253                        if (accountCount < MAX_CACHED_ACCOUNTS) {
1254                            mc = new MatrixCursor(projection, 1);
1255                            mc.addRow(new Object[] {accountCount});
1256                            c = mc;
1257                            break;
1258                        }
1259                    }
1260                    c = db.query(tableName, projection,
1261                            selection, selectionArgs, null, null, sortOrder, limit);
1262                    break;
1263                case BODY_ID:
1264                case MESSAGE_ID:
1265                case DELETED_MESSAGE_ID:
1266                case UPDATED_MESSAGE_ID:
1267                case ATTACHMENT_ID:
1268                case MAILBOX_ID:
1269                case ACCOUNT_ID:
1270                case HOSTAUTH_ID:
1271                case POLICY_ID:
1272                case QUICK_RESPONSE_ID:
1273                    id = uri.getPathSegments().get(1);
1274                    if (cache != null) {
1275                        c = cache.getCachedCursor(id, projection);
1276                    }
1277                    if (c == null) {
1278                        CacheToken token = null;
1279                        if (cache != null) {
1280                            token = cache.getCacheToken(id);
1281                        }
1282                        c = db.query(tableName, projection, whereWithId(id, selection),
1283                                selectionArgs, null, null, sortOrder, limit);
1284                        if (cache != null) {
1285                            c = cache.putCursor(c, id, projection, token);
1286                        }
1287                    }
1288                    break;
1289                case ATTACHMENTS_MESSAGE_ID:
1290                    // All attachments for the given message
1291                    id = uri.getPathSegments().get(2);
1292                    c = db.query(Attachment.TABLE_NAME, projection,
1293                            whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
1294                            selectionArgs, null, null, sortOrder, limit);
1295                    break;
1296                case QUICK_RESPONSE_ACCOUNT_ID:
1297                    // All quick responses for the given account
1298                    id = uri.getPathSegments().get(2);
1299                    c = db.query(QuickResponse.TABLE_NAME, projection,
1300                            whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection),
1301                            selectionArgs, null, null, sortOrder);
1302                    break;
1303                default:
1304                    throw new IllegalArgumentException("Unknown URI " + uri);
1305            }
1306        } catch (SQLiteException e) {
1307            checkDatabases();
1308            throw e;
1309        } catch (RuntimeException e) {
1310            checkDatabases();
1311            e.printStackTrace();
1312            throw e;
1313        } finally {
1314            if (cache != null && c != null && Email.DEBUG) {
1315                cache.recordQueryTime(c, System.nanoTime() - time);
1316            }
1317            if (c == null) {
1318                // This should never happen, but let's be sure to log it...
1319                Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection);
1320            }
1321        }
1322
1323        if ((c != null) && !isTemporary()) {
1324            c.setNotificationUri(getContext().getContentResolver(), uri);
1325        }
1326        return c;
1327    }
1328
1329    private String whereWithId(String id, String selection) {
1330        StringBuilder sb = new StringBuilder(256);
1331        sb.append("_id=");
1332        sb.append(id);
1333        if (selection != null) {
1334            sb.append(" AND (");
1335            sb.append(selection);
1336            sb.append(')');
1337        }
1338        return sb.toString();
1339    }
1340
1341    /**
1342     * Combine a locally-generated selection with a user-provided selection
1343     *
1344     * This introduces risk that the local selection might insert incorrect chars
1345     * into the SQL, so use caution.
1346     *
1347     * @param where locally-generated selection, must not be null
1348     * @param selection user-provided selection, may be null
1349     * @return a single selection string
1350     */
1351    private String whereWith(String where, String selection) {
1352        if (selection == null) {
1353            return where;
1354        }
1355        StringBuilder sb = new StringBuilder(where);
1356        sb.append(" AND (");
1357        sb.append(selection);
1358        sb.append(')');
1359
1360        return sb.toString();
1361    }
1362
1363    /**
1364     * Restore a HostAuth from a database, given its unique id
1365     * @param db the database
1366     * @param id the unique id (_id) of the row
1367     * @return a fully populated HostAuth or null if the row does not exist
1368     */
1369    private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
1370        Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
1371                HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null);
1372        try {
1373            if (c.moveToFirst()) {
1374                HostAuth hostAuth = new HostAuth();
1375                hostAuth.restore(c);
1376                return hostAuth;
1377            }
1378            return null;
1379        } finally {
1380            c.close();
1381        }
1382    }
1383
1384    /**
1385     * Copy the Account and HostAuth tables from one database to another
1386     * @param fromDatabase the source database
1387     * @param toDatabase the destination database
1388     * @return the number of accounts copied, or -1 if an error occurred
1389     */
1390    private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
1391        if (fromDatabase == null || toDatabase == null) return -1;
1392        int copyCount = 0;
1393        try {
1394            // Lock both databases; for the "from" database, we don't want anyone changing it from
1395            // under us; for the "to" database, we want to make the operation atomic
1396            fromDatabase.beginTransaction();
1397            toDatabase.beginTransaction();
1398            // Delete anything hanging around here
1399            toDatabase.delete(Account.TABLE_NAME, null, null);
1400            toDatabase.delete(HostAuth.TABLE_NAME, null, null);
1401            // Get our account cursor
1402            Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
1403                    null, null, null, null, null);
1404            boolean noErrors = true;
1405            try {
1406                // Loop through accounts, copying them and associated host auth's
1407                while (c.moveToNext()) {
1408                    Account account = new Account();
1409                    account.restore(c);
1410
1411                    // Clear security sync key and sync key, as these were specific to the state of
1412                    // the account, and we've reset that...
1413                    // Clear policy key so that we can re-establish policies from the server
1414                    // TODO This is pretty EAS specific, but there's a lot of that around
1415                    account.mSecuritySyncKey = null;
1416                    account.mSyncKey = null;
1417                    account.mPolicyKey = 0;
1418
1419                    // Copy host auth's and update foreign keys
1420                    HostAuth hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeyRecv);
1421                    // The account might have gone away, though very unlikely
1422                    if (hostAuth == null) continue;
1423                    account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
1424                            hostAuth.toContentValues());
1425                    // EAS accounts have no send HostAuth
1426                    if (account.mHostAuthKeySend > 0) {
1427                        hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
1428                        // Belt and suspenders; I can't imagine that this is possible, since we
1429                        // checked the validity of the account above, and the database is now locked
1430                        if (hostAuth == null) continue;
1431                        account.mHostAuthKeySend = toDatabase.insert(HostAuth.TABLE_NAME, null,
1432                                hostAuth.toContentValues());
1433                    }
1434                    // Now, create the account in the "to" database
1435                    toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
1436                    copyCount++;
1437                }
1438            } catch (SQLiteException e) {
1439                noErrors = false;
1440                copyCount = -1;
1441            } finally {
1442                fromDatabase.endTransaction();
1443                if (noErrors) {
1444                    // Say it's ok to commit
1445                    toDatabase.setTransactionSuccessful();
1446                }
1447                toDatabase.endTransaction();
1448                c.close();
1449            }
1450        } catch (SQLiteException e) {
1451            copyCount = -1;
1452        }
1453        return copyCount;
1454    }
1455
1456    private static SQLiteDatabase getBackupDatabase(Context context) {
1457        DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME);
1458        return helper.getWritableDatabase();
1459    }
1460
1461    /**
1462     * Backup account data, returning the number of accounts backed up
1463     */
1464    private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
1465        if (Email.DEBUG) {
1466            Log.d(TAG, "backupAccounts...");
1467        }
1468        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1469        try {
1470            int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
1471            if (numBackedUp < 0) {
1472                Log.e(TAG, "Account backup failed!");
1473            } else if (Email.DEBUG) {
1474                Log.d(TAG, "Backed up " + numBackedUp + " accounts...");
1475            }
1476            return numBackedUp;
1477        } finally {
1478            if (backupDatabase != null) {
1479                backupDatabase.close();
1480            }
1481        }
1482    }
1483
1484    /**
1485     * Restore account data, returning the number of accounts restored
1486     */
1487    private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
1488        if (Email.DEBUG) {
1489            Log.d(TAG, "restoreAccounts...");
1490        }
1491        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1492        try {
1493            int numRecovered = copyAccountTables(backupDatabase, mainDatabase);
1494            if (numRecovered > 0) {
1495                Log.e(TAG, "Recovered " + numRecovered + " accounts!");
1496            } else if (numRecovered < 0) {
1497                Log.e(TAG, "Account recovery failed?");
1498            } else if (Email.DEBUG) {
1499                Log.d(TAG, "No accounts to restore...");
1500            }
1501            return numRecovered;
1502        } finally {
1503            if (backupDatabase != null) {
1504                backupDatabase.close();
1505            }
1506        }
1507    }
1508
1509    @Override
1510    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1511        // Handle this special case the fastest possible way
1512        if (uri == INTEGRITY_CHECK_URI) {
1513            checkDatabases();
1514            return 0;
1515        } else if (uri == ACCOUNT_BACKUP_URI) {
1516            return backupAccounts(getContext(), getDatabase(getContext()));
1517        }
1518
1519        // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
1520        Uri notificationUri = EmailContent.CONTENT_URI;
1521
1522        int match = findMatch(uri, "update");
1523        Context context = getContext();
1524        ContentResolver resolver = context.getContentResolver();
1525        // See the comment at delete(), above
1526        SQLiteDatabase db = getDatabase(context);
1527        int table = match >> BASE_SHIFT;
1528        int result;
1529
1530        // We do NOT allow setting of unreadCount/messageCount via the provider
1531        // These columns are maintained via triggers
1532        if (match == MAILBOX_ID || match == MAILBOX) {
1533            values.remove(MailboxColumns.UNREAD_COUNT);
1534            values.remove(MailboxColumns.MESSAGE_COUNT);
1535        }
1536
1537        ContentCache cache = mContentCaches[table];
1538        String tableName = TABLE_NAMES[table];
1539        String id = "0";
1540
1541        try {
1542            if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
1543                if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
1544                    notifyUIConversation(uri);
1545                }
1546            }
1547outer:
1548            switch (match) {
1549                case UI_ATTACHMENT:
1550                    return uiUpdateAttachment(uri, values);
1551                case UI_MESSAGE:
1552                    return uiUpdateMessage(uri, values);
1553                case MAILBOX_ID_ADD_TO_FIELD:
1554                case ACCOUNT_ID_ADD_TO_FIELD:
1555                    id = uri.getPathSegments().get(1);
1556                    String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME);
1557                    Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME);
1558                    if (field == null || add == null) {
1559                        throw new IllegalArgumentException("No field/add specified " + uri);
1560                    }
1561                    ContentValues actualValues = new ContentValues();
1562                    if (cache != null) {
1563                        cache.lock(id);
1564                    }
1565                    try {
1566                        db.beginTransaction();
1567                        try {
1568                            Cursor c = db.query(tableName,
1569                                    new String[] {EmailContent.RECORD_ID, field},
1570                                    whereWithId(id, selection),
1571                                    selectionArgs, null, null, null);
1572                            try {
1573                                result = 0;
1574                                String[] bind = new String[1];
1575                                if (c.moveToNext()) {
1576                                    bind[0] = c.getString(0); // _id
1577                                    long value = c.getLong(1) + add;
1578                                    actualValues.put(field, value);
1579                                    result = db.update(tableName, actualValues, ID_EQUALS, bind);
1580                                }
1581                                db.setTransactionSuccessful();
1582                            } finally {
1583                                c.close();
1584                            }
1585                        } finally {
1586                            db.endTransaction();
1587                        }
1588                    } finally {
1589                        if (cache != null) {
1590                            cache.unlock(id, actualValues);
1591                        }
1592                    }
1593                    break;
1594                case SYNCED_MESSAGE_ID:
1595                case UPDATED_MESSAGE_ID:
1596                case MESSAGE_ID:
1597                case BODY_ID:
1598                case ATTACHMENT_ID:
1599                case MAILBOX_ID:
1600                case ACCOUNT_ID:
1601                case HOSTAUTH_ID:
1602                case QUICK_RESPONSE_ID:
1603                case POLICY_ID:
1604                    id = uri.getPathSegments().get(1);
1605                    if (cache != null) {
1606                        cache.lock(id);
1607                    }
1608                    try {
1609                        if (match == SYNCED_MESSAGE_ID) {
1610                            // For synced messages, first copy the old message to the updated table
1611                            // Note the insert or ignore semantics, guaranteeing that only the first
1612                            // update will be reflected in the updated message table; therefore this
1613                            // row will always have the "original" data
1614                            db.execSQL(UPDATED_MESSAGE_INSERT + id);
1615                        } else if (match == MESSAGE_ID) {
1616                            db.execSQL(UPDATED_MESSAGE_DELETE + id);
1617                        }
1618                        result = db.update(tableName, values, whereWithId(id, selection),
1619                                selectionArgs);
1620                    } catch (SQLiteException e) {
1621                        // Null out values (so they aren't cached) and re-throw
1622                        values = null;
1623                        throw e;
1624                    } finally {
1625                        if (cache != null) {
1626                            cache.unlock(id, values);
1627                        }
1628                    }
1629                    if (match == ATTACHMENT_ID) {
1630                        long attId = Integer.parseInt(id);
1631                        if (values.containsKey(Attachment.FLAGS)) {
1632                            int flags = values.getAsInteger(Attachment.FLAGS);
1633                            mAttachmentService.attachmentChanged(context, attId, flags);
1634                        }
1635                        // Notify UI if necessary; there are only two columns we can change that
1636                        // would be worth a notification
1637                        if (values.containsKey(AttachmentColumns.UI_STATE) ||
1638                                values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
1639                            // Notify on individual attachment
1640                            notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
1641                            Attachment att = Attachment.restoreAttachmentWithId(context, attId);
1642                            if (att != null) {
1643                                // And on owning Message
1644                                notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
1645                            }
1646                        }
1647                    } else if (match == MAILBOX_ID && values.containsKey(Mailbox.UI_SYNC_STATUS)) {
1648                        notifyUI(UIPROVIDER_MAILBOX_NOTIFIER, id);
1649                        // TODO: Remove logging
1650                        Log.d(TAG, "Notifying mailbox " + id + " status: " +
1651                                values.getAsInteger(Mailbox.UI_SYNC_STATUS));
1652                    } else if (match == ACCOUNT_ID) {
1653                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
1654                    }
1655                    break;
1656                case BODY:
1657                case MESSAGE:
1658                case UPDATED_MESSAGE:
1659                case ATTACHMENT:
1660                case MAILBOX:
1661                case ACCOUNT:
1662                case HOSTAUTH:
1663                case POLICY:
1664                    switch(match) {
1665                        // To avoid invalidating the cache on updates, we execute them one at a
1666                        // time using the XXX_ID uri; these are all executed atomically
1667                        case ACCOUNT:
1668                        case MAILBOX:
1669                        case HOSTAUTH:
1670                        case POLICY:
1671                            Cursor c = db.query(tableName, EmailContent.ID_PROJECTION,
1672                                    selection, selectionArgs, null, null, null);
1673                            db.beginTransaction();
1674                            result = 0;
1675                            try {
1676                                while (c.moveToNext()) {
1677                                    update(ContentUris.withAppendedId(
1678                                                uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)),
1679                                            values, null, null);
1680                                    result++;
1681                                }
1682                                db.setTransactionSuccessful();
1683                            } finally {
1684                                db.endTransaction();
1685                                c.close();
1686                            }
1687                            break outer;
1688                        // Any cached table other than those above should be invalidated here
1689                        case MESSAGE:
1690                            // If we're doing some generic update, the whole cache needs to be
1691                            // invalidated.  This case should be quite rare
1692                            cache.invalidate("Update", uri, selection);
1693                            //$FALL-THROUGH$
1694                        default:
1695                            result = db.update(tableName, values, selection, selectionArgs);
1696                            break outer;
1697                    }
1698                case ACCOUNT_RESET_NEW_COUNT_ID:
1699                    id = uri.getPathSegments().get(1);
1700                    if (cache != null) {
1701                        cache.lock(id);
1702                    }
1703                    ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
1704                    if (values != null) {
1705                        Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME);
1706                        if (set != null) {
1707                            newMessageCount = new ContentValues();
1708                            newMessageCount.put(Account.NEW_MESSAGE_COUNT, set);
1709                        }
1710                    }
1711                    try {
1712                        result = db.update(tableName, newMessageCount,
1713                                whereWithId(id, selection), selectionArgs);
1714                    } finally {
1715                        if (cache != null) {
1716                            cache.unlock(id, values);
1717                        }
1718                    }
1719                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1720                    break;
1721                case ACCOUNT_RESET_NEW_COUNT:
1722                    result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
1723                            selection, selectionArgs);
1724                    // Affects all accounts.  Just invalidate all account cache.
1725                    cache.invalidate("Reset all new counts", null, null);
1726                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1727                    break;
1728                default:
1729                    throw new IllegalArgumentException("Unknown URI " + uri);
1730            }
1731        } catch (SQLiteException e) {
1732            checkDatabases();
1733            throw e;
1734        }
1735
1736        // Notify all notifier cursors
1737        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
1738
1739        resolver.notifyChange(notificationUri, null);
1740        return result;
1741    }
1742
1743    /**
1744     * Returns the base notification URI for the given content type.
1745     *
1746     * @param match The type of content that was modified.
1747     */
1748    private Uri getBaseNotificationUri(int match) {
1749        Uri baseUri = null;
1750        switch (match) {
1751            case MESSAGE:
1752            case MESSAGE_ID:
1753            case SYNCED_MESSAGE_ID:
1754                baseUri = Message.NOTIFIER_URI;
1755                break;
1756            case ACCOUNT:
1757            case ACCOUNT_ID:
1758                baseUri = Account.NOTIFIER_URI;
1759                break;
1760        }
1761        return baseUri;
1762    }
1763
1764    /**
1765     * Sends a change notification to any cursors observers of the given base URI. The final
1766     * notification URI is dynamically built to contain the specified information. It will be
1767     * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
1768     * upon the given values.
1769     * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
1770     * If this is necessary, it can be added. However, due to the implementation of
1771     * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
1772     *
1773     * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
1774     * @param op Optional operation to be appended to the URI.
1775     * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
1776     *           appended to the base URI.
1777     */
1778    private void sendNotifierChange(Uri baseUri, String op, String id) {
1779        if (baseUri == null) return;
1780
1781        final ContentResolver resolver = getContext().getContentResolver();
1782
1783        // Append the operation, if specified
1784        if (op != null) {
1785            baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
1786        }
1787
1788        long longId = 0L;
1789        try {
1790            longId = Long.valueOf(id);
1791        } catch (NumberFormatException ignore) {}
1792        if (longId > 0) {
1793            resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null);
1794        } else {
1795            resolver.notifyChange(baseUri, null);
1796        }
1797
1798        // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
1799        if (baseUri.equals(Message.NOTIFIER_URI)) {
1800            sendMessageListDataChangedNotification();
1801        }
1802    }
1803
1804    private void sendMessageListDataChangedNotification() {
1805        final Context context = getContext();
1806        final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
1807        // Ideally this intent would contain information about which account changed, to limit the
1808        // updates to that particular account.  Unfortunately, that information is not available in
1809        // sendNotifierChange().
1810        context.sendBroadcast(intent);
1811    }
1812
1813    @Override
1814    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1815            throws OperationApplicationException {
1816        Context context = getContext();
1817        SQLiteDatabase db = getDatabase(context);
1818        db.beginTransaction();
1819        try {
1820            ContentProviderResult[] results = super.applyBatch(operations);
1821            db.setTransactionSuccessful();
1822            return results;
1823        } finally {
1824            db.endTransaction();
1825        }
1826    }
1827
1828    /**
1829     * For testing purposes, check whether a given row is cached
1830     * @param baseUri the base uri of the EmailContent
1831     * @param id the row id of the EmailContent
1832     * @return whether or not the row is currently cached
1833     */
1834    @VisibleForTesting
1835    protected boolean isCached(Uri baseUri, long id) {
1836        int match = findMatch(baseUri, "isCached");
1837        int table = match >> BASE_SHIFT;
1838        ContentCache cache = mContentCaches[table];
1839        if (cache == null) return false;
1840        Cursor cc = cache.get(Long.toString(id));
1841        return (cc != null);
1842    }
1843
1844    public static interface AttachmentService {
1845        /**
1846         * Notify the service that an attachment has changed.
1847         */
1848        void attachmentChanged(Context context, long id, int flags);
1849    }
1850
1851    private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() {
1852        @Override
1853        public void attachmentChanged(Context context, long id, int flags) {
1854            // The default implementation delegates to the real service.
1855            AttachmentDownloadService.attachmentChanged(context, id, flags);
1856        }
1857    };
1858    private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
1859
1860    /**
1861     * Injects a custom attachment service handler. If null is specified, will reset to the
1862     * default service.
1863     */
1864    public void injectAttachmentService(AttachmentService as) {
1865        mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as;
1866    }
1867
1868    // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message,
1869    //   (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey
1870    //   FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1)))
1871    //      AS Boxes
1872    // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount
1873    //   OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey)
1874    // TODO: This query can be simplified a bit
1875    private static final String NOTIFICATION_QUERY =
1876        "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT +
1877            ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" +
1878        " FROM " +
1879            Message.TABLE_NAME + "," +
1880            "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," +
1881                MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT +
1882                "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME +
1883                " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" +
1884                " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR ("
1885                + MailboxColumns.SYNC_INTERVAL + "!=0 AND " +
1886                MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " +
1887        "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." +
1888                MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." +
1889                MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY +
1890                " AND " + MessageColumns.FLAG_READ + "=0";
1891
1892    public Cursor notificationQuery(Uri uri) {
1893        SQLiteDatabase db = getDatabase(getContext());
1894        String accountId = uri.getLastPathSegment();
1895        return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId});
1896   }
1897
1898    public Cursor mostRecentMessageQuery(Uri uri) {
1899        SQLiteDatabase db = getDatabase(getContext());
1900        String mailboxId = uri.getLastPathSegment();
1901        return db.rawQuery("select max(_id) from Message where mailboxKey=?",
1902                new String[] {mailboxId});
1903   }
1904
1905    /**
1906     * Support for UnifiedEmail below
1907     */
1908
1909    private static final String NOT_A_DRAFT_STRING =
1910        Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
1911
1912    /**
1913     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
1914     * conversation list in UnifiedEmail)
1915     */
1916    private static final ProjectionMap sMessageListMap = ProjectionMap.builder()
1917        .add(BaseColumns._ID, MessageColumns.ID)
1918        .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
1919        .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
1920        .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
1921        .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
1922        .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
1923        .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
1924        .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
1925        .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
1926        .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
1927        .add(UIProvider.ConversationColumns.SENDING_STATE,
1928                Integer.toString(ConversationSendingState.OTHER))
1929        .add(UIProvider.ConversationColumns.PRIORITY, Integer.toString(ConversationPriority.LOW))
1930        .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
1931        .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
1932            .add(UIProvider.ConversationColumns.FOLDER_LIST,
1933                    "'content://" + EmailContent.AUTHORITY + "/uifolder/' || "
1934                            + MessageColumns.MAILBOX_KEY)
1935        .build();
1936
1937    /**
1938     * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
1939     * UnifiedEmail
1940     */
1941    private static final ProjectionMap sMessageViewMap = ProjectionMap.builder()
1942        .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID)
1943        .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
1944        .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
1945        .add(UIProvider.MessageColumns.CONVERSATION_ID,
1946                uriWithFQId("uimessage", Message.TABLE_NAME))
1947        .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT)
1948        .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET)
1949        .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST)
1950        .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST)
1951        .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST)
1952        .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST)
1953        .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST)
1954        .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP)
1955        .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT)
1956        .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT)
1957        .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0")
1958        .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
1959        .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
1960        .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
1961        .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT)
1962        .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
1963                uriWithFQId("uiattachments", Message.TABLE_NAME))
1964        .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0")
1965        .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI,
1966                uriWithFQId("uiupdatedraft", Message.TABLE_NAME))
1967        .add(UIProvider.MessageColumns.SEND_MESSAGE_URI,
1968                uriWithFQId("uisenddraft", Message.TABLE_NAME))
1969        // TODO(pwestbro): make this actually return valid results.
1970        .add(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, "0")
1971        .build();
1972
1973    /**
1974     * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail
1975     */
1976    private static String getFolderCapabilities() {
1977        return "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
1978                ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
1979                " ELSE 0 END";
1980    }
1981
1982    private static final ProjectionMap sFolderListMap = ProjectionMap.builder()
1983        .add(BaseColumns._ID, MailboxColumns.ID)
1984        .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
1985        .add(UIProvider.FolderColumns.NAME, "displayName")
1986        .add(UIProvider.FolderColumns.HAS_CHILDREN,
1987                MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
1988        .add(UIProvider.FolderColumns.CAPABILITIES, getFolderCapabilities())
1989        .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
1990        .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
1991        .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
1992        .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
1993        .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT)
1994        .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh"))
1995        .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
1996        .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
1997        .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.TOTAL_COUNT)
1998        .build();
1999
2000    private static final ProjectionMap sAccountListMap = ProjectionMap.builder()
2001        .add(BaseColumns._ID, AccountColumns.ID)
2002        .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
2003        .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
2004        .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft"))
2005        .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail"))
2006        .add(UIProvider.AccountColumns.UNDO_URI, uriWithId("uiundo"))
2007        .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
2008        .add(UIProvider.AccountColumns.SETTINGS_QUERY_URI, uriWithId("uisettings"))
2009        .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
2010        // TODO: Is this used?
2011        .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
2012        .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
2013        .build();
2014
2015    /**
2016     * The "ORDER BY" clause for top level folders
2017     */
2018    private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
2019        + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
2020        + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
2021        + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
2022        + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
2023        + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
2024        + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
2025        // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
2026        + " ELSE 10 END"
2027        + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
2028
2029
2030    /**
2031     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
2032     * conversation list in UnifiedEmail)
2033     */
2034    private static final ProjectionMap sAccountSettingsMap = ProjectionMap.builder()
2035        .add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
2036        .add(UIProvider.SettingsColumns.AUTO_ADVANCE,
2037                Integer.toString(UIProvider.AutoAdvance.OLDER))
2038        .add(UIProvider.SettingsColumns.MESSAGE_TEXT_SIZE,
2039                Integer.toString(UIProvider.MessageTextSize.NORMAL))
2040        .add(UIProvider.SettingsColumns.SNAP_HEADERS,
2041                Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
2042        .add(UIProvider.SettingsColumns.REPLY_BEHAVIOR,
2043                Integer.toString(UIProvider.DefaultReplyBehavior.REPLY))
2044        .add(UIProvider.SettingsColumns.HIDE_CHECKBOXES, "0")
2045        .add(UIProvider.SettingsColumns.CONFIRM_DELETE, "0")
2046        .add(UIProvider.SettingsColumns.CONFIRM_ARCHIVE, "0")
2047        .add(UIProvider.SettingsColumns.CONFIRM_SEND, "0")
2048        .build();
2049
2050    /**
2051     * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
2052     */
2053    private static final ProjectionMap sAttachmentMap = ProjectionMap.builder()
2054        .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
2055        .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
2056        .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
2057        .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
2058        .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
2059        .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
2060        .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, AttachmentColumns.UI_DOWNLOADED_SIZE)
2061        .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
2062        .build();
2063
2064    /**
2065     * Generate the SELECT clause using a specified mapping and the original UI projection
2066     * @param map the ProjectionMap to use for this projection
2067     * @param projection the projection as sent by UnifiedEmail
2068     * @param values ContentValues to be used if the ProjectionMap entry is null
2069     * @return a StringBuilder containing the SELECT expression for a SQLite query
2070     */
2071    private StringBuilder genSelect(ProjectionMap map, String[] projection) {
2072        return genSelect(map, projection, EMPTY_CONTENT_VALUES);
2073    }
2074
2075    private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) {
2076        StringBuilder sb = new StringBuilder("SELECT ");
2077        boolean first = true;
2078        for (String column: projection) {
2079            if (first) {
2080                first = false;
2081            } else {
2082                sb.append(',');
2083            }
2084            String val = null;
2085            // First look at values; this is an override of default behavior
2086            if (values.containsKey(column)) {
2087                val = "'" + values.getAsString(column) + "' AS " + column;
2088            } else {
2089                // Now, get the standard value for the column from our projection map
2090                val = map.get(column);
2091                // If we don't have the column, return "NULL AS <column>", and warn
2092                if (val == null) {
2093                    Log.w(TAG, "UIProvider column not found, returning NULL: " + column);
2094                    val = "NULL AS " + column;
2095                }
2096            }
2097            sb.append(val);
2098        }
2099        return sb;
2100    }
2101
2102    /**
2103     * Convenience method to create a Uri string given the "type" of query; we append the type
2104     * of the query and the id column name (_id)
2105     *
2106     * @param type the "type" of the query, as defined by our UriMatcher definitions
2107     * @return a Uri string
2108     */
2109    private static String uriWithId(String type) {
2110        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id";
2111    }
2112
2113    /**
2114     * Convenience method to create a Uri string given the "type" of query and the table name to
2115     * which it applies; we append the type of the query and the fully qualified (FQ) id column
2116     * (i.e. including the table name); we need this for join queries where _id would otherwise
2117     * be ambiguous
2118     *
2119     * @param type the "type" of the query, as defined by our UriMatcher definitions
2120     * @param tableName the name of the table whose _id is referred to
2121     * @return a Uri string
2122     */
2123    private static String uriWithFQId(String type, String tableName) {
2124        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
2125    }
2126
2127    /**
2128     * Generate the "view message" SQLite query, given a projection from UnifiedEmail
2129     *
2130     * @param uiProjection as passed from UnifiedEmail
2131     * @return the SQLite query to be executed on the EmailProvider database
2132     */
2133    private String genQueryViewMessage(String[] uiProjection, String id) {
2134        Context context = getContext();
2135        long messageId = Long.parseLong(id);
2136        Message msg = Message.restoreMessageWithId(context, messageId);
2137        if (msg != null && (msg.mFlagLoaded == Message.FLAG_LOADED_PARTIAL)) {
2138            EmailServiceProxy service =
2139                    EmailServiceUtils.getServiceForAccount(context, null, msg.mAccountKey);
2140            try {
2141                service.loadMore(messageId);
2142            } catch (RemoteException e) {
2143                // Nothing to do
2144            }
2145        }
2146        StringBuilder sb = genSelect(sMessageViewMap, uiProjection);
2147        sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " +
2148                Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " +
2149                Message.TABLE_NAME + "." + Message.RECORD_ID + "=?");
2150        return sb.toString();
2151    }
2152
2153    /**
2154     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
2155     *
2156     * @param uiProjection as passed from UnifiedEmail
2157     * @return the SQLite query to be executed on the EmailProvider database
2158     */
2159    private String genQueryMailboxMessages(String[] uiProjection) {
2160        StringBuilder sb = genSelect(sMessageListMap, uiProjection);
2161        // Make constant
2162        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " +
2163                MessageColumns.TIMESTAMP + " DESC");
2164        return sb.toString();
2165    }
2166
2167    /**
2168     * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
2169     *
2170     * @param uiProjection as passed from UnifiedEmail
2171     * @return the SQLite query to be executed on the EmailProvider database
2172     */
2173    private String genQueryAccountMailboxes(String[] uiProjection) {
2174        StringBuilder sb = genSelect(sFolderListMap, uiProjection);
2175        // Make constant
2176        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
2177                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
2178                " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
2179        sb.append(MAILBOX_ORDER_BY);
2180        return sb.toString();
2181    }
2182
2183    /**
2184     * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
2185     *
2186     * @param uiProjection as passed from UnifiedEmail
2187     * @return the SQLite query to be executed on the EmailProvider database
2188     */
2189    private String genQueryMailbox(String[] uiProjection, String id) {
2190        long mailboxId = Long.parseLong(id);
2191        ContentValues values = EMPTY_CONTENT_VALUES;
2192        if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
2193            // This is the current search mailbox; use the total count
2194            values = new ContentValues();
2195            values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount);
2196            // "load more" is valid for search results
2197            values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2198                    uiUriString("uiloadmore", mailboxId));
2199        } else {
2200            Context context = getContext();
2201            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
2202            String protocol = Account.getProtocol(context, mailbox.mAccountKey);
2203            // "load more" is valid for IMAP/POP3
2204            if (HostAuth.SCHEME_IMAP.equals(protocol) || HostAuth.SCHEME_POP3.equals(protocol)) {
2205                values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2206                        uiUriString("uiloadmore", mailboxId));
2207            }
2208        }
2209        StringBuilder sb = genSelect(sFolderListMap, uiProjection, values);
2210        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?");
2211        return sb.toString();
2212    }
2213
2214    private static final long IMAP_CAPABILITIES =
2215            AccountCapabilities.SYNCABLE_FOLDERS |
2216            AccountCapabilities.FOLDER_SERVER_SEARCH |
2217            AccountCapabilities.UNDO;
2218
2219    private static final long POP3_CAPABILITIES = 0;
2220
2221    private static final long EAS_12_CAPABILITIES =
2222            AccountCapabilities.SYNCABLE_FOLDERS |
2223            AccountCapabilities.FOLDER_SERVER_SEARCH |
2224            AccountCapabilities.SANITIZED_HTML |
2225            AccountCapabilities.SMART_REPLY |
2226            AccountCapabilities.SERVER_SEARCH |
2227            AccountCapabilities.UNDO;
2228
2229    private static final long EAS_2_CAPABILITIES =
2230            AccountCapabilities.SYNCABLE_FOLDERS |
2231            AccountCapabilities.SANITIZED_HTML |
2232            AccountCapabilities.SMART_REPLY |
2233            AccountCapabilities.UNDO;
2234
2235    private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com");
2236
2237    private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
2238
2239    private static String getExternalUriString(String segment, String account) {
2240        return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
2241                .appendQueryParameter("account", account).build().toString();
2242    }
2243
2244    private static String getExternalUriStringEmail2(String segment, String account) {
2245        return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
2246                .appendQueryParameter("account", account).build().toString();
2247    }
2248
2249    /**
2250     * Generate a "single account" SQLite query, given a projection from UnifiedEmail
2251     *
2252     * @param uiProjection as passed from UnifiedEmail
2253     * @return the SQLite query to be executed on the EmailProvider database
2254     */
2255    // TODO: Get protocol specific stuff out of here (it should be in the account)
2256    private String genQueryAccount(String[] uiProjection, String id) {
2257        ContentValues values = new ContentValues();
2258        long accountId = Long.parseLong(id);
2259        String protocol = Account.getProtocol(getContext(), accountId);
2260        if (HostAuth.SCHEME_IMAP.equals(protocol)) {
2261            values.put(UIProvider.AccountColumns.CAPABILITIES, IMAP_CAPABILITIES);
2262        } else if (HostAuth.SCHEME_POP3.equals(protocol)) {
2263            values.put(UIProvider.AccountColumns.CAPABILITIES, POP3_CAPABILITIES);
2264        } else {
2265            Account account = Account.restoreAccountWithId(getContext(), accountId);
2266            String easVersion = account.mProtocolVersion;
2267            Double easVersionDouble = 2.5D;
2268            if (easVersion != null) {
2269                try {
2270                    easVersionDouble = Double.parseDouble(easVersion);
2271                } catch (NumberFormatException e) {
2272                    // Stick with 2.5
2273                }
2274            }
2275            if (easVersionDouble >= 12.0D) {
2276                values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_12_CAPABILITIES);
2277            } else {
2278                values.put(UIProvider.AccountColumns.CAPABILITIES, EAS_2_CAPABILITIES);
2279            }
2280        }
2281        values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
2282                getExternalUriString("settings", id));
2283        values.put(UIProvider.AccountColumns.COMPOSE_URI,
2284                getExternalUriStringEmail2("compose", id));
2285        values.put(UIProvider.AccountColumns.MIME_TYPE, "application/email-ls");
2286        StringBuilder sb = genSelect(sAccountListMap, uiProjection, values);
2287        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
2288        return sb.toString();
2289    }
2290
2291    /**
2292     * Generate an "account settings" SQLite query, given a projection from UnifiedEmail
2293     *
2294     * @param uiProjection as passed from UnifiedEmail
2295     * @return the SQLite query to be executed on the EmailProvider database
2296     */
2297    private String genQuerySettings(String[] uiProjection, String id) {
2298        ContentValues values = new ContentValues();
2299        long accountId = Long.parseLong(id);
2300        long mailboxId = Mailbox.findMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
2301        if (mailboxId != Mailbox.NO_MAILBOX) {
2302            values.put(UIProvider.SettingsColumns.DEFAULT_INBOX,
2303                    uiUriString("uifolder", mailboxId));
2304        }
2305        StringBuilder sb = genSelect(sAccountSettingsMap, uiProjection, values);
2306        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
2307        return sb.toString();
2308    }
2309
2310    private Cursor uiAccounts(String[] uiProjection) {
2311        Context context = getContext();
2312        SQLiteDatabase db = getDatabase(context);
2313        Cursor accountIdCursor =
2314                db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
2315        MatrixCursor mc = new MatrixCursor(uiProjection, accountIdCursor.getCount());
2316        Object[] values = new Object[uiProjection.length];
2317        try {
2318            while (accountIdCursor.moveToNext()) {
2319                String id = accountIdCursor.getString(0);
2320                Cursor accountCursor =
2321                        db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
2322                if (accountCursor.moveToNext()) {
2323                    for (int i = 0; i < uiProjection.length; i++) {
2324                        values[i] = accountCursor.getString(i);
2325                    }
2326                    mc.addRow(values);
2327                }
2328                accountCursor.close();
2329            }
2330        } finally {
2331            accountIdCursor.close();
2332        }
2333        return mc;
2334    }
2335
2336    /**
2337     * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
2338     *
2339     * @param uiProjection as passed from UnifiedEmail
2340     * @return the SQLite query to be executed on the EmailProvider database
2341     */
2342    private String genQueryAttachments(String[] uiProjection) {
2343        StringBuilder sb = genSelect(sAttachmentMap, uiProjection);
2344        sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY +
2345                " =? ");
2346        return sb.toString();
2347    }
2348
2349    /**
2350     * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
2351     *
2352     * @param uiProjection as passed from UnifiedEmail
2353     * @return the SQLite query to be executed on the EmailProvider database
2354     */
2355    private String genQueryAttachment(String[] uiProjection) {
2356        StringBuilder sb = genSelect(sAttachmentMap, uiProjection);
2357        sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? ");
2358        return sb.toString();
2359    }
2360
2361    /**
2362     * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
2363     *
2364     * @param uiProjection as passed from UnifiedEmail
2365     * @return the SQLite query to be executed on the EmailProvider database
2366     */
2367    private String genQuerySubfolders(String[] uiProjection) {
2368        StringBuilder sb = genSelect(sFolderListMap, uiProjection);
2369        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
2370                " =? ORDER BY ");
2371        sb.append(MAILBOX_ORDER_BY);
2372        return sb.toString();
2373    }
2374
2375    /**
2376     * Handle UnifiedEmail queries here (dispatched from query())
2377     *
2378     * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
2379     * @param uri the original uri passed in from UnifiedEmail
2380     * @param uiProjection the projection passed in from UnifiedEmail
2381     * @return the result Cursor
2382     */
2383    private Cursor uiQuery(int match, Uri uri, String[] uiProjection) {
2384        Context context = getContext();
2385        ContentResolver resolver = context.getContentResolver();
2386        SQLiteDatabase db = getDatabase(context);
2387        // Should we ever return null, or throw an exception??
2388        Cursor c = null;
2389        String id = uri.getPathSegments().get(1);
2390        Uri notifyUri = null;
2391        switch(match) {
2392            case UI_FOLDERS:
2393                c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id});
2394                break;
2395            case UI_SUBFOLDERS:
2396                c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id});
2397                break;
2398            case UI_MESSAGES:
2399                c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id});
2400                notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
2401                break;
2402            case UI_MESSAGE:
2403                c = db.rawQuery(genQueryViewMessage(uiProjection, id), new String[] {id});
2404                break;
2405            case UI_ATTACHMENTS:
2406                c = db.rawQuery(genQueryAttachments(uiProjection), new String[] {id});
2407                notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
2408                break;
2409            case UI_ATTACHMENT:
2410                c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id});
2411                notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
2412                break;
2413            case UI_FOLDER:
2414                c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id});
2415                notifyUri = UIPROVIDER_MAILBOX_NOTIFIER.buildUpon().appendPath(id).build();
2416                break;
2417            case UI_ACCOUNT:
2418                c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
2419                notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
2420                break;
2421            case UI_SETTINGS:
2422                c = db.rawQuery(genQuerySettings(uiProjection, id), new String[] {id});
2423                notifyUri = UIPROVIDER_SETTINGS_NOTIFIER.buildUpon().appendPath(id).build();
2424                break;
2425        }
2426        if (notifyUri != null) {
2427            c.setNotificationUri(resolver, notifyUri);
2428        }
2429        return c;
2430    }
2431
2432    /**
2433     * Create a mailbox given the account and mailboxType.
2434     */
2435    private Mailbox createMailbox(long accountId, int mailboxType) {
2436        Context context = getContext();
2437        int resId = -1;
2438        switch (mailboxType) {
2439            case Mailbox.TYPE_INBOX:
2440                resId = R.string.mailbox_name_server_inbox;
2441                break;
2442            case Mailbox.TYPE_OUTBOX:
2443                resId = R.string.mailbox_name_server_outbox;
2444                break;
2445            case Mailbox.TYPE_DRAFTS:
2446                resId = R.string.mailbox_name_server_drafts;
2447                break;
2448            case Mailbox.TYPE_TRASH:
2449                resId = R.string.mailbox_name_server_trash;
2450                break;
2451            case Mailbox.TYPE_SENT:
2452                resId = R.string.mailbox_name_server_sent;
2453                break;
2454            case Mailbox.TYPE_JUNK:
2455                resId = R.string.mailbox_name_server_junk;
2456                break;
2457            default:
2458                throw new IllegalArgumentException("Illegal mailbox type");
2459        }
2460        Log.d(TAG, "Creating mailbox of type " + mailboxType + " for account " + accountId);
2461        Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, context.getString(resId));
2462        // Make sure drafts and save will show up in recents...
2463        // If these already exist (from old Email app), they will have touch times
2464        switch (mailboxType) {
2465            case Mailbox.TYPE_DRAFTS:
2466                box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
2467                break;
2468            case Mailbox.TYPE_SENT:
2469                box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
2470                break;
2471        }
2472        box.save(context);
2473        return box;
2474    }
2475
2476    /**
2477     * Given an account name and a mailbox type, return that mailbox, creating it if necessary
2478     * @param accountName the account name to use
2479     * @param mailboxType the type of mailbox we're trying to find
2480     * @return the mailbox of the given type for the account in the uri, or null if not found
2481     */
2482    private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) {
2483        long id = Long.parseLong(accountId);
2484        Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType);
2485        if (mailbox == null) {
2486            mailbox = createMailbox(id, mailboxType);
2487        }
2488        return mailbox;
2489    }
2490
2491    private Message getMessageFromPathSegments(List<String> pathSegments) {
2492        Message msg = null;
2493        if (pathSegments.size() > 2) {
2494            msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2)));
2495        }
2496        if (msg == null) {
2497            msg = new Message();
2498        }
2499        return msg;
2500    }
2501
2502    private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) {
2503        if (value instanceof Integer) {
2504            Integer intValue = (Integer)value;
2505            values.put(columnName, intValue);
2506        } else if (value instanceof Boolean) {
2507            Boolean boolValue = (Boolean)value;
2508            values.put(columnName, boolValue ? 1 : 0);
2509        } else if (value instanceof Long) {
2510            Long longValue = (Long)value;
2511            values.put(columnName, longValue);
2512        }
2513    }
2514
2515    private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
2516        Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
2517        if (stateValue != null) {
2518            // This is a command from UIProvider
2519            long attachmentId = Long.parseLong(uri.getLastPathSegment());
2520            Context context = getContext();
2521            Attachment attachment =
2522                    Attachment.restoreAttachmentWithId(context, attachmentId);
2523            if (attachment == null) {
2524                // Went away; ah, well...
2525                return 0;
2526            }
2527            ContentValues values = new ContentValues();
2528            switch (stateValue.intValue()) {
2529                case UIProvider.AttachmentState.NOT_SAVED:
2530                    // Set state, try to cancel request
2531                    values.put(AttachmentColumns.UI_STATE, stateValue);
2532                    values.put(AttachmentColumns.FLAGS,
2533                            attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
2534                    attachment.update(context, values);
2535                    return 1;
2536                case UIProvider.AttachmentState.DOWNLOADING:
2537                    // Set state and destination; request download
2538                    values.put(AttachmentColumns.UI_STATE, stateValue);
2539                    Integer destinationValue =
2540                        uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
2541                    values.put(AttachmentColumns.UI_DESTINATION,
2542                            destinationValue == null ? 0 : destinationValue);
2543                    values.put(AttachmentColumns.FLAGS,
2544                            attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
2545                    attachment.update(context, values);
2546                    return 1;
2547            }
2548        }
2549        return 0;
2550    }
2551
2552    private ContentValues convertUiMessageValues(ContentValues values) {
2553        ContentValues ourValues = new ContentValues();
2554        for (String columnName: values.keySet()) {
2555            Object val = values.get(columnName);
2556            if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
2557                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
2558            } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
2559                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
2560            } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
2561                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
2562            } else if (columnName.equals(UIProvider.ConversationColumns.FOLDER_LIST)) {
2563                // Convert from folder list uri to mailbox key
2564                Uri uri = Uri.parse((String)val);
2565                Long mailboxId = Long.parseLong(uri.getLastPathSegment());
2566                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
2567            } else {
2568                throw new IllegalArgumentException("Can't update " + columnName + " in message");
2569            }
2570        }
2571        return ourValues;
2572    }
2573
2574    private Uri convertToEmailProviderUri(Uri uri, boolean asProvider) {
2575        String idString = uri.getLastPathSegment();
2576        try {
2577            long id = Long.parseLong(idString);
2578            Uri ourUri = ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id);
2579            if (asProvider) {
2580                ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
2581            }
2582            return ourUri;
2583        } catch (NumberFormatException e) {
2584            return null;
2585        }
2586    }
2587
2588    private Message getMessageFromLastSegment(Uri uri) {
2589        long messageId = Long.parseLong(uri.getLastPathSegment());
2590        return Message.restoreMessageWithId(getContext(), messageId);
2591    }
2592
2593    /**
2594     * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
2595     * clear out the undo list and start over
2596     * @param uri the uri we're working on
2597     * @param op the ContentProviderOperation to perform upon undo
2598     */
2599    private void addToSequence(Uri uri, ContentProviderOperation op) {
2600        String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
2601        if (sequenceString != null) {
2602            int sequence = Integer.parseInt(sequenceString);
2603            if (sequence > mLastSequence) {
2604                // Reset sequence
2605                mLastSequenceOps.clear();
2606                mLastSequence = sequence;
2607            }
2608            // TODO: Need something to indicate a change isn't ready (undoable)
2609            mLastSequenceOps.add(op);
2610        }
2611    }
2612
2613    private int uiUpdateMessage(Uri uri, ContentValues values) {
2614        Uri ourUri = convertToEmailProviderUri(uri, true);
2615        if (ourUri == null) return 0;
2616        ContentValues ourValues = convertUiMessageValues(values);
2617        Message msg = getMessageFromLastSegment(uri);
2618        if (msg == null) return 0;
2619        ContentValues undoValues = new ContentValues();
2620        for (String columnName: ourValues.keySet()) {
2621            if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
2622                undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
2623            } else if (columnName.equals(MessageColumns.FLAG_READ)) {
2624                undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
2625            } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
2626                undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
2627            }
2628        }
2629        ContentProviderOperation op =
2630                ContentProviderOperation.newUpdate(convertToEmailProviderUri(uri, false))
2631                        .withValues(undoValues)
2632                        .build();
2633        addToSequence(uri, op);
2634        return update(ourUri, ourValues, null, null);
2635    }
2636
2637    private int uiDeleteMessage(Uri uri) {
2638        Context context = getContext();
2639        Message msg = getMessageFromLastSegment(uri);
2640        if (msg == null) return 0;
2641        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
2642        if (mailbox == null) return 0;
2643        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
2644            // We actually delete these, including attachments
2645            AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
2646            return context.getContentResolver().delete(
2647                    ContentUris.withAppendedId(Message.CONTENT_URI, msg.mId), null, null);
2648        }
2649        Mailbox trashMailbox =
2650                Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
2651        if (trashMailbox == null) return 0;
2652        ContentValues values = new ContentValues();
2653        values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
2654        return uiUpdateMessage(uri, values);
2655    }
2656
2657    private Cursor uiUndo(Uri uri, String[] projection) {
2658        // First see if we have any operations saved
2659        // TODO: Make sure seq matches
2660        if (!mLastSequenceOps.isEmpty()) {
2661            try {
2662                // TODO Always use this projection?  Or what's passed in?
2663                // Not sure if UI wants it, but I'm making a cursor of convo uri's
2664                MatrixCursor c = new MatrixCursor(
2665                        new String[] {UIProvider.ConversationColumns.URI},
2666                        mLastSequenceOps.size());
2667                for (ContentProviderOperation op: mLastSequenceOps) {
2668                    c.addRow(new String[] {op.getUri().toString()});
2669                }
2670                // Just apply the batch and we're done!
2671                applyBatch(mLastSequenceOps);
2672                // But clear the operations
2673                mLastSequenceOps.clear();
2674                // Tell the UI there are changes
2675                getContext().getContentResolver().notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER,
2676                        null);
2677                Log.d(TAG, "[Notify UI: Undo]");
2678                return c;
2679            } catch (OperationApplicationException e) {
2680            }
2681        }
2682        return new MatrixCursor(projection, 0);
2683    }
2684
2685    private void notifyUIConversation(Uri uri) {
2686        String id = uri.getLastPathSegment();
2687        Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
2688        if (msg != null) {
2689            notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(msg.mMailboxKey));
2690        }
2691    }
2692
2693    private void notifyUIConversationMailbox(long id) {
2694        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
2695    }
2696
2697    private void notifyUI(Uri uri, String id) {
2698        Uri notifyUri = uri.buildUpon().appendPath(id).build();
2699        getContext().getContentResolver().notifyChange(notifyUri, null);
2700        // Temporary
2701        Log.d(TAG, "[Notify UI: " + notifyUri + "]");
2702    }
2703
2704    private void notifyUI(Uri uri, long id) {
2705        notifyUI(uri, Long.toString(id));
2706    }
2707
2708    /**
2709     * Support for services and service notifications
2710     */
2711
2712    private final IEmailServiceCallback.Stub mServiceCallback =
2713            new IEmailServiceCallback.Stub() {
2714
2715        @Override
2716        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
2717                throws RemoteException {
2718        }
2719
2720        @Override
2721        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
2722                throws RemoteException {
2723            // We'll get callbacks here from the services, which we'll pass back to the UI
2724            Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId);
2725            EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null);
2726        }
2727
2728        @Override
2729        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
2730                int progress) throws RemoteException {
2731        }
2732
2733        @Override
2734        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
2735                throws RemoteException {
2736        }
2737
2738        @Override
2739        public void loadMessageStatus(long messageId, int statusCode, int progress)
2740                throws RemoteException {
2741        }
2742    };
2743
2744    private Cursor uiFolderRefresh(Uri uri) {
2745        Context context = getContext();
2746        String idString = uri.getLastPathSegment();
2747        long id = Long.parseLong(idString);
2748        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id);
2749        if (mailbox == null) return null;
2750        EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context,
2751                mServiceCallback, mailbox.mAccountKey);
2752        try {
2753            service.startSync(id, true);
2754        } catch (RemoteException e) {
2755        }
2756        return null;
2757    }
2758
2759    //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
2760    public static final int VISIBLE_LIMIT_INCREMENT = 10;
2761    //Number of additional messages to load when a user selects "Load more..." in a search
2762    public static final int SEARCH_MORE_INCREMENT = 10;
2763
2764    private Cursor uiFolderLoadMore(Uri uri) {
2765        Context context = getContext();
2766        String idString = uri.getLastPathSegment();
2767        long id = Long.parseLong(idString);
2768        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id);
2769        if (mailbox == null) return null;
2770        if (mailbox.mType == Mailbox.TYPE_SEARCH) {
2771            // Ask for 10 more messages
2772            mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
2773            runSearchQuery(context, mailbox.mAccountKey, id);
2774        } else {
2775            ContentValues values = new ContentValues();
2776            values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
2777            values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT);
2778            Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id);
2779            // Increase the limit
2780            context.getContentResolver().update(mailboxUri, values, null, null);
2781            // And order a refresh
2782            uiFolderRefresh(uri);
2783        }
2784        return null;
2785    }
2786
2787    private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
2788    private SearchParams mSearchParams;
2789
2790    /**
2791     * Returns the search mailbox for the specified account, creating one if necessary
2792     * @return the search mailbox for the passed in account
2793     */
2794    private Mailbox getSearchMailbox(long accountId) {
2795        Context context = getContext();
2796        Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
2797        if (m == null) {
2798            m = new Mailbox();
2799            m.mAccountKey = accountId;
2800            m.mServerId = SEARCH_MAILBOX_SERVER_ID;
2801            m.mFlagVisible = false;
2802            m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
2803            m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
2804            m.mType = Mailbox.TYPE_SEARCH;
2805            m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
2806            m.mParentKey = Mailbox.NO_MAILBOX;
2807            m.save(context);
2808        }
2809        return m;
2810    }
2811
2812    private void runSearchQuery(final Context context, final long accountId,
2813            final long searchMailboxId) {
2814        // Start the search running in the background
2815        new Thread(new Runnable() {
2816            @Override
2817            public void run() {
2818                 try {
2819                    EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context,
2820                            mServiceCallback, accountId);
2821                    if (service != null) {
2822                        try {
2823                            // Save away the total count
2824                            mSearchParams.mTotalCount = service.searchMessages(accountId,
2825                                    mSearchParams, searchMailboxId);
2826                            Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount);
2827                            notifyUI(UIPROVIDER_MAILBOX_NOTIFIER, searchMailboxId);
2828                        } catch (RemoteException e) {
2829                            Log.e("searchMessages", "RemoteException", e);
2830                        }
2831                    }
2832                } finally {
2833                }
2834            }}).start();
2835
2836    }
2837
2838    // TODO: Handle searching for more...
2839    private Cursor uiSearch(Uri uri, String[] projection) {
2840        final long accountId = Long.parseLong(uri.getLastPathSegment());
2841
2842        // TODO: Check the actual mailbox
2843        Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
2844        if (inbox == null) return null;
2845
2846        String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
2847        if (filter == null) {
2848            throw new IllegalArgumentException("No query parameter in search query");
2849        }
2850
2851        // Find/create our search mailbox
2852        Mailbox searchMailbox = getSearchMailbox(accountId);
2853        final long searchMailboxId = searchMailbox.mId;
2854
2855        mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
2856
2857        final Context context = getContext();
2858        if (mSearchParams.mOffset == 0) {
2859            // Delete existing contents of search mailbox
2860            ContentResolver resolver = context.getContentResolver();
2861            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
2862                    null);
2863            ContentValues cv = new ContentValues();
2864            // For now, use the actual query as the name of the mailbox
2865            cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
2866            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
2867                    cv, null, null);
2868        }
2869
2870        // Start the search running in the background
2871        runSearchQuery(context, accountId, searchMailboxId);
2872
2873        // This will look just like a "normal" folder
2874        return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
2875                searchMailbox.mId), projection);
2876    }
2877
2878    private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
2879    private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
2880        MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
2881        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
2882    private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?";
2883
2884    /**
2885     * Delete an account and clean it up
2886     */
2887    private int uiDeleteAccount(Uri uri) {
2888        Context context = getContext();
2889        long accountId = Long.parseLong(uri.getLastPathSegment());
2890        try {
2891            // Get the account URI.
2892            final Account account = Account.restoreAccountWithId(context, accountId);
2893            if (account == null) {
2894                return 0; // Already deleted?
2895            }
2896
2897            deleteAccountData(context, accountId);
2898
2899            // Now delete the account itself
2900            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
2901            context.getContentResolver().delete(uri, null, null);
2902
2903            // Clean up
2904            AccountBackupRestore.backup(context);
2905            SecurityPolicy.getInstance(context).reducePolicies();
2906            Email.setServicesEnabledSync(context);
2907            return 1;
2908        } catch (Exception e) {
2909            Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
2910        }
2911        return 0;
2912    }
2913
2914    private int uiDeleteAccountData(Uri uri) {
2915        Context context = getContext();
2916        long accountId = Long.parseLong(uri.getLastPathSegment());
2917        // Get the account URI.
2918        final Account account = Account.restoreAccountWithId(context, accountId);
2919        if (account == null) {
2920            return 0; // Already deleted?
2921        }
2922        deleteAccountData(context, accountId);
2923        return 1;
2924    }
2925
2926    private void deleteAccountData(Context context, long accountId) {
2927        // Delete synced attachments
2928        AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
2929
2930        // Delete synced email, leaving only an empty inbox.  We do this in two phases:
2931        // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
2932        // 2. Delete all remaining messages (which will be the inbox messages)
2933        ContentResolver resolver = context.getContentResolver();
2934        String[] accountIdArgs = new String[] { Long.toString(accountId) };
2935        resolver.delete(Mailbox.CONTENT_URI,
2936                MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
2937                accountIdArgs);
2938        resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
2939
2940        // Delete sync keys on remaining items
2941        ContentValues cv = new ContentValues();
2942        cv.putNull(Account.SYNC_KEY);
2943        resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
2944        cv.clear();
2945        cv.putNull(Mailbox.SYNC_KEY);
2946        resolver.update(Mailbox.CONTENT_URI, cv,
2947                MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
2948
2949        // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
2950        IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId);
2951        if (service != null) {
2952            try {
2953                service.deleteAccountPIMData(accountId);
2954            } catch (RemoteException e) {
2955                // Can't do anything about this
2956            }
2957        }
2958    }
2959}
2960