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