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