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