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 com.android.email.Email;
     20 import com.android.email.provider.EmailContent.Account;
     21 import com.android.email.provider.EmailContent.AccountColumns;
     22 import com.android.email.provider.EmailContent.Attachment;
     23 import com.android.email.provider.EmailContent.AttachmentColumns;
     24 import com.android.email.provider.EmailContent.Body;
     25 import com.android.email.provider.EmailContent.BodyColumns;
     26 import com.android.email.provider.EmailContent.HostAuth;
     27 import com.android.email.provider.EmailContent.HostAuthColumns;
     28 import com.android.email.provider.EmailContent.Mailbox;
     29 import com.android.email.provider.EmailContent.MailboxColumns;
     30 import com.android.email.provider.EmailContent.Message;
     31 import com.android.email.provider.EmailContent.MessageColumns;
     32 import com.android.email.provider.EmailContent.SyncColumns;
     33 
     34 import android.accounts.AccountManager;
     35 import android.content.ContentProvider;
     36 import android.content.ContentProviderOperation;
     37 import android.content.ContentProviderResult;
     38 import android.content.ContentUris;
     39 import android.content.ContentValues;
     40 import android.content.Context;
     41 import android.content.OperationApplicationException;
     42 import android.content.UriMatcher;
     43 import android.database.Cursor;
     44 import android.database.SQLException;
     45 import android.database.sqlite.SQLiteDatabase;
     46 import android.database.sqlite.SQLiteException;
     47 import android.database.sqlite.SQLiteOpenHelper;
     48 import android.net.Uri;
     49 import android.util.Log;
     50 
     51 import java.io.File;
     52 import java.util.ArrayList;
     53 
     54 public class EmailProvider extends ContentProvider {
     55 
     56     private static final String TAG = "EmailProvider";
     57 
     58     protected static final String DATABASE_NAME = "EmailProvider.db";
     59     protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
     60 
     61     public static final Uri INTEGRITY_CHECK_URI =
     62         Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck");
     63 
     64     // Definitions for our queries looking for orphaned messages
     65     private static final String[] ORPHANS_PROJECTION
     66         = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY};
     67     private static final int ORPHANS_ID = 0;
     68     private static final int ORPHANS_MAILBOX_KEY = 1;
     69 
     70     private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
     71 
     72     // Any changes to the database format *must* include update-in-place code.
     73     // Original version: 3
     74     // Version 4: Database wipe required; changing AccountManager interface w/Exchange
     75     // Version 5: Database wipe required; changing AccountManager interface w/Exchange
     76     // Version 6: Adding Message.mServerTimeStamp column
     77     // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages
     78     //            from the Message_Deletes and Message_Updates tables
     79     // Version 8: Add security flags column to accounts table
     80     // Version 9: Add security sync key and signature to accounts table
     81     // Version 10: Add meeting info to message table
     82     // Version 11: Add content and flags to attachment table
     83     // Version 12: Add content_bytes to attachment table. content is deprecated.
     84     public static final int DATABASE_VERSION = 12;
     85 
     86     // Any changes to the database format *must* include update-in-place code.
     87     // Original version: 2
     88     // Version 3: Add "sourceKey" column
     89     // Version 4: Database wipe required; changing AccountManager interface w/Exchange
     90     // Version 5: Database wipe required; changing AccountManager interface w/Exchange
     91     // Version 6: Adding Body.mIntroText column
     92     public static final int BODY_DATABASE_VERSION = 6;
     93 
     94     public static final String EMAIL_AUTHORITY = "com.android.email.provider";
     95 
     96     private static final int ACCOUNT_BASE = 0;
     97     private static final int ACCOUNT = ACCOUNT_BASE;
     98     private static final int ACCOUNT_MAILBOXES = ACCOUNT_BASE + 1;
     99     private static final int ACCOUNT_ID = ACCOUNT_BASE + 2;
    100     private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 3;
    101 
    102     private static final int MAILBOX_BASE = 0x1000;
    103     private static final int MAILBOX = MAILBOX_BASE;
    104     private static final int MAILBOX_MESSAGES = MAILBOX_BASE + 1;
    105     private static final int MAILBOX_ID = MAILBOX_BASE + 2;
    106     private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3;
    107 
    108     private static final int MESSAGE_BASE = 0x2000;
    109     private static final int MESSAGE = MESSAGE_BASE;
    110     private static final int MESSAGE_ID = MESSAGE_BASE + 1;
    111     private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
    112 
    113     private static final int ATTACHMENT_BASE = 0x3000;
    114     private static final int ATTACHMENT = ATTACHMENT_BASE;
    115     private static final int ATTACHMENT_CONTENT = ATTACHMENT_BASE + 1;
    116     private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 2;
    117     private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 3;
    118 
    119     private static final int HOSTAUTH_BASE = 0x4000;
    120     private static final int HOSTAUTH = HOSTAUTH_BASE;
    121     private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
    122 
    123     private static final int UPDATED_MESSAGE_BASE = 0x5000;
    124     private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
    125     private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
    126 
    127     private static final int DELETED_MESSAGE_BASE = 0x6000;
    128     private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
    129     private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
    130     private static final int DELETED_MESSAGE_MAILBOX = DELETED_MESSAGE_BASE + 2;
    131 
    132     // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS
    133     private static final int LAST_EMAIL_PROVIDER_DB_BASE = DELETED_MESSAGE_BASE;
    134 
    135     // DO NOT CHANGE BODY_BASE!!
    136     private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000;
    137     private static final int BODY = BODY_BASE;
    138     private static final int BODY_ID = BODY_BASE + 1;
    139     private static final int BODY_MESSAGE_ID = BODY_BASE + 2;
    140     private static final int BODY_HTML = BODY_BASE + 3;
    141     private static final int BODY_TEXT = BODY_BASE + 4;
    142 
    143 
    144     private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
    145 
    146     private static final String[] TABLE_NAMES = {
    147         EmailContent.Account.TABLE_NAME,
    148         EmailContent.Mailbox.TABLE_NAME,
    149         EmailContent.Message.TABLE_NAME,
    150         EmailContent.Attachment.TABLE_NAME,
    151         EmailContent.HostAuth.TABLE_NAME,
    152         EmailContent.Message.UPDATED_TABLE_NAME,
    153         EmailContent.Message.DELETED_TABLE_NAME,
    154         EmailContent.Body.TABLE_NAME
    155     };
    156 
    157     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    158 
    159     /**
    160      * Let's only generate these SQL strings once, as they are used frequently
    161      * Note that this isn't relevant for table creation strings, since they are used only once
    162      */
    163     private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
    164         Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    165         EmailContent.RECORD_ID + '=';
    166 
    167     private static final String UPDATED_MESSAGE_DELETE = "delete from " +
    168         Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '=';
    169 
    170     private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
    171         Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
    172         EmailContent.RECORD_ID + '=';
    173 
    174     private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
    175         " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY +
    176         " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " +
    177         Message.TABLE_NAME + ')';
    178 
    179     private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
    180         " where " + BodyColumns.MESSAGE_KEY + '=';
    181 
    182     private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?";
    183 
    184     private static final String TRIGGER_MAILBOX_DELETE =
    185         "create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME +
    186         " begin" +
    187         " delete from " + Message.TABLE_NAME +
    188         "  where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
    189         "; delete from " + Message.UPDATED_TABLE_NAME +
    190         "  where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
    191         "; delete from " + Message.DELETED_TABLE_NAME +
    192         "  where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
    193         "; end";
    194 
    195     static {
    196         // Email URI matching table
    197         UriMatcher matcher = sURIMatcher;
    198 
    199         // All accounts
    200         matcher.addURI(EMAIL_AUTHORITY, "account", ACCOUNT);
    201         // A specific account
    202         // insert into this URI causes a mailbox to be added to the account
    203         matcher.addURI(EMAIL_AUTHORITY, "account/#", ACCOUNT_ID);
    204         // The mailboxes in a specific account
    205         matcher.addURI(EMAIL_AUTHORITY, "account/#/mailbox", ACCOUNT_MAILBOXES);
    206 
    207         // All mailboxes
    208         matcher.addURI(EMAIL_AUTHORITY, "mailbox", MAILBOX);
    209         // A specific mailbox
    210         // insert into this URI causes a message to be added to the mailbox
    211         // ** NOTE For now, the accountKey must be set manually in the values!
    212         matcher.addURI(EMAIL_AUTHORITY, "mailbox/#", MAILBOX_ID);
    213         // The messages in a specific mailbox
    214         matcher.addURI(EMAIL_AUTHORITY, "mailbox/#/message", MAILBOX_MESSAGES);
    215 
    216         // All messages
    217         matcher.addURI(EMAIL_AUTHORITY, "message", MESSAGE);
    218         // A specific message
    219         // insert into this URI causes an attachment to be added to the message
    220         matcher.addURI(EMAIL_AUTHORITY, "message/#", MESSAGE_ID);
    221 
    222         // A specific attachment
    223         matcher.addURI(EMAIL_AUTHORITY, "attachment", ATTACHMENT);
    224         // A specific attachment (the header information)
    225         matcher.addURI(EMAIL_AUTHORITY, "attachment/#", ATTACHMENT_ID);
    226         // The content for a specific attachment
    227         // NOT IMPLEMENTED
    228         matcher.addURI(EMAIL_AUTHORITY, "attachment/content/*", ATTACHMENT_CONTENT);
    229         // The attachments of a specific message (query only) (insert & delete TBD)
    230         matcher.addURI(EMAIL_AUTHORITY, "attachment/message/#", ATTACHMENTS_MESSAGE_ID);
    231 
    232         // All mail bodies
    233         matcher.addURI(EMAIL_AUTHORITY, "body", BODY);
    234         // A specific mail body
    235         matcher.addURI(EMAIL_AUTHORITY, "body/#", BODY_ID);
    236         // The body for a specific message
    237         matcher.addURI(EMAIL_AUTHORITY, "body/message/#", BODY_MESSAGE_ID);
    238         // The HTML part of a specific mail body
    239         matcher.addURI(EMAIL_AUTHORITY, "body/#/html", BODY_HTML);
    240         // The plain text part of a specific mail body
    241         matcher.addURI(EMAIL_AUTHORITY, "body/#/text", BODY_TEXT);
    242 
    243         // All hostauth records
    244         matcher.addURI(EMAIL_AUTHORITY, "hostauth", HOSTAUTH);
    245         // A specific hostauth
    246         matcher.addURI(EMAIL_AUTHORITY, "hostauth/#", HOSTAUTH_ID);
    247 
    248         // Atomically a constant value to a particular field of a mailbox/account
    249         matcher.addURI(EMAIL_AUTHORITY, "mailboxIdAddToField/#", MAILBOX_ID_ADD_TO_FIELD);
    250         matcher.addURI(EMAIL_AUTHORITY, "accountIdAddToField/#", ACCOUNT_ID_ADD_TO_FIELD);
    251 
    252         /**
    253          * THIS URI HAS SPECIAL SEMANTICS
    254          * ITS USE IS INTENDED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK
    255          * TO A SERVER VIA A SYNC ADAPTER
    256          */
    257         matcher.addURI(EMAIL_AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
    258 
    259         /**
    260          * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
    261          * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
    262          * BY THE UI APPLICATION
    263          */
    264         // All deleted messages
    265         matcher.addURI(EMAIL_AUTHORITY, "deletedMessage", DELETED_MESSAGE);
    266         // A specific deleted message
    267         matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
    268         // All deleted messages from a specific mailbox
    269         // NOT IMPLEMENTED; do we need this as a convenience?
    270         matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/mailbox/#", DELETED_MESSAGE_MAILBOX);
    271 
    272         // All updated messages
    273         matcher.addURI(EMAIL_AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
    274         // A specific updated message
    275         matcher.addURI(EMAIL_AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
    276     }
    277 
    278     /*
    279      * Internal helper method for index creation.
    280      * Example:
    281      * "create index message_" + MessageColumns.FLAG_READ
    282      * + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");"
    283      */
    284     /* package */
    285     static String createIndex(String tableName, String columnName) {
    286         return "create index " + tableName.toLowerCase() + '_' + columnName
    287             + " on " + tableName + " (" + columnName + ");";
    288     }
    289 
    290     static void createMessageTable(SQLiteDatabase db) {
    291         String messageColumns = MessageColumns.DISPLAY_NAME + " text, "
    292             + MessageColumns.TIMESTAMP + " integer, "
    293             + MessageColumns.SUBJECT + " text, "
    294             + MessageColumns.FLAG_READ + " integer, "
    295             + MessageColumns.FLAG_LOADED + " integer, "
    296             + MessageColumns.FLAG_FAVORITE + " integer, "
    297             + MessageColumns.FLAG_ATTACHMENT + " integer, "
    298             + MessageColumns.FLAGS + " integer, "
    299             + MessageColumns.CLIENT_ID + " integer, "
    300             + MessageColumns.MESSAGE_ID + " text, "
    301             + MessageColumns.MAILBOX_KEY + " integer, "
    302             + MessageColumns.ACCOUNT_KEY + " integer, "
    303             + MessageColumns.FROM_LIST + " text, "
    304             + MessageColumns.TO_LIST + " text, "
    305             + MessageColumns.CC_LIST + " text, "
    306             + MessageColumns.BCC_LIST + " text, "
    307             + MessageColumns.REPLY_TO_LIST + " text, "
    308             + MessageColumns.MEETING_INFO + " text"
    309             + ");";
    310 
    311         // This String and the following String MUST have the same columns, except for the type
    312         // of those columns!
    313         String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    314             + SyncColumns.SERVER_ID + " text, "
    315             + SyncColumns.SERVER_TIMESTAMP + " integer, "
    316             + messageColumns;
    317 
    318         // For the updated and deleted tables, the id is assigned, but we do want to keep track
    319         // of the ORDER of updates using an autoincrement primary key.  We use the DATA column
    320         // at this point; it has no other function
    321         String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, "
    322             + SyncColumns.SERVER_ID + " text, "
    323             + SyncColumns.SERVER_TIMESTAMP + " integer, "
    324             + messageColumns;
    325 
    326         // The three tables have the same schema
    327         db.execSQL("create table " + Message.TABLE_NAME + createString);
    328         db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString);
    329         db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString);
    330 
    331         String indexColumns[] = {
    332             MessageColumns.TIMESTAMP,
    333             MessageColumns.FLAG_READ,
    334             MessageColumns.FLAG_LOADED,
    335             MessageColumns.MAILBOX_KEY,
    336             SyncColumns.SERVER_ID
    337         };
    338 
    339         for (String columnName : indexColumns) {
    340             db.execSQL(createIndex(Message.TABLE_NAME, columnName));
    341         }
    342 
    343         // Deleting a Message deletes all associated Attachments
    344         // Deleting the associated Body cannot be done in a trigger, because the Body is stored
    345         // in a separate database, and trigger cannot operate on attached databases.
    346         db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME +
    347                 " begin delete from " + Attachment.TABLE_NAME +
    348                 "  where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID +
    349                 "; end");
    350 
    351         // Add triggers to keep unread count accurate per mailbox
    352 
    353         // Insert a message; if flagRead is zero, add to the unread count of the message's mailbox
    354         db.execSQL("create trigger unread_message_insert before insert on " + Message.TABLE_NAME +
    355                 " when NEW." + MessageColumns.FLAG_READ + "=0" +
    356                 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
    357                 '=' + MailboxColumns.UNREAD_COUNT + "+1" +
    358                 "  where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
    359                 "; end");
    360 
    361         // Delete a message; if flagRead is zero, decrement the unread count of the msg's mailbox
    362         db.execSQL("create trigger unread_message_delete before delete on " + Message.TABLE_NAME +
    363                 " when OLD." + MessageColumns.FLAG_READ + "=0" +
    364                 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
    365                 '=' + MailboxColumns.UNREAD_COUNT + "-1" +
    366                 "  where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
    367                 "; end");
    368 
    369         // Change a message's mailbox
    370         db.execSQL("create trigger unread_message_move before update of " +
    371                 MessageColumns.MAILBOX_KEY + " on " + Message.TABLE_NAME +
    372                 " when OLD." + MessageColumns.FLAG_READ + "=0" +
    373                 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
    374                 '=' + MailboxColumns.UNREAD_COUNT + "-1" +
    375                 "  where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
    376                 "; update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
    377                 '=' + MailboxColumns.UNREAD_COUNT + "+1" +
    378                 " where " + EmailContent.RECORD_ID + "=NEW." + MessageColumns.MAILBOX_KEY +
    379                 "; end");
    380 
    381         // Change a message's read state
    382         db.execSQL("create trigger unread_message_read before update of " +
    383                 MessageColumns.FLAG_READ + " on " + Message.TABLE_NAME +
    384                 " when OLD." + MessageColumns.FLAG_READ + "!=NEW." + MessageColumns.FLAG_READ +
    385                 " begin update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UNREAD_COUNT +
    386                 '=' + MailboxColumns.UNREAD_COUNT + "+ case OLD." + MessageColumns.FLAG_READ +
    387                 " when 0 then -1 else 1 end" +
    388                 "  where " + EmailContent.RECORD_ID + "=OLD." + MessageColumns.MAILBOX_KEY +
    389                 "; end");
    390    }
    391 
    392     static void resetMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    393         try {
    394             db.execSQL("drop table " + Message.TABLE_NAME);
    395             db.execSQL("drop table " + Message.UPDATED_TABLE_NAME);
    396             db.execSQL("drop table " + Message.DELETED_TABLE_NAME);
    397         } catch (SQLException e) {
    398         }
    399         createMessageTable(db);
    400     }
    401 
    402     static void createAccountTable(SQLiteDatabase db) {
    403         String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    404             + AccountColumns.DISPLAY_NAME + " text, "
    405             + AccountColumns.EMAIL_ADDRESS + " text, "
    406             + AccountColumns.SYNC_KEY + " text, "
    407             + AccountColumns.SYNC_LOOKBACK + " integer, "
    408             + AccountColumns.SYNC_INTERVAL + " text, "
    409             + AccountColumns.HOST_AUTH_KEY_RECV + " integer, "
    410             + AccountColumns.HOST_AUTH_KEY_SEND + " integer, "
    411             + AccountColumns.FLAGS + " integer, "
    412             + AccountColumns.IS_DEFAULT + " integer, "
    413             + AccountColumns.COMPATIBILITY_UUID + " text, "
    414             + AccountColumns.SENDER_NAME + " text, "
    415             + AccountColumns.RINGTONE_URI + " text, "
    416             + AccountColumns.PROTOCOL_VERSION + " text, "
    417             + AccountColumns.NEW_MESSAGE_COUNT + " integer, "
    418             + AccountColumns.SECURITY_FLAGS + " integer, "
    419             + AccountColumns.SECURITY_SYNC_KEY + " text, "
    420             + AccountColumns.SIGNATURE + " text "
    421             + ");";
    422         db.execSQL("create table " + Account.TABLE_NAME + s);
    423         // Deleting an account deletes associated Mailboxes and HostAuth's
    424         db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME +
    425                 " begin delete from " + Mailbox.TABLE_NAME +
    426                 " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID +
    427                 "; delete from " + HostAuth.TABLE_NAME +
    428                 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV +
    429                 "; delete from " + HostAuth.TABLE_NAME +
    430                 " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND +
    431         "; end");
    432     }
    433 
    434     static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    435         try {
    436             db.execSQL("drop table " +  Account.TABLE_NAME);
    437         } catch (SQLException e) {
    438         }
    439         createAccountTable(db);
    440     }
    441 
    442     static void createHostAuthTable(SQLiteDatabase db) {
    443         String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    444             + HostAuthColumns.PROTOCOL + " text, "
    445             + HostAuthColumns.ADDRESS + " text, "
    446             + HostAuthColumns.PORT + " integer, "
    447             + HostAuthColumns.FLAGS + " integer, "
    448             + HostAuthColumns.LOGIN + " text, "
    449             + HostAuthColumns.PASSWORD + " text, "
    450             + HostAuthColumns.DOMAIN + " text, "
    451             + HostAuthColumns.ACCOUNT_KEY + " integer"
    452             + ");";
    453         db.execSQL("create table " + HostAuth.TABLE_NAME + s);
    454     }
    455 
    456     static void resetHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    457         try {
    458             db.execSQL("drop table " + HostAuth.TABLE_NAME);
    459         } catch (SQLException e) {
    460         }
    461         createHostAuthTable(db);
    462     }
    463 
    464     static void createMailboxTable(SQLiteDatabase db) {
    465         String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    466             + MailboxColumns.DISPLAY_NAME + " text, "
    467             + MailboxColumns.SERVER_ID + " text, "
    468             + MailboxColumns.PARENT_SERVER_ID + " text, "
    469             + MailboxColumns.ACCOUNT_KEY + " integer, "
    470             + MailboxColumns.TYPE + " integer, "
    471             + MailboxColumns.DELIMITER + " integer, "
    472             + MailboxColumns.SYNC_KEY + " text, "
    473             + MailboxColumns.SYNC_LOOKBACK + " integer, "
    474             + MailboxColumns.SYNC_INTERVAL + " integer, "
    475             + MailboxColumns.SYNC_TIME + " integer, "
    476             + MailboxColumns.UNREAD_COUNT + " integer, "
    477             + MailboxColumns.FLAG_VISIBLE + " integer, "
    478             + MailboxColumns.FLAGS + " integer, "
    479             + MailboxColumns.VISIBLE_LIMIT + " integer, "
    480             + MailboxColumns.SYNC_STATUS + " text"
    481             + ");";
    482         db.execSQL("create table " + Mailbox.TABLE_NAME + s);
    483         db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID
    484                 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")");
    485         db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY
    486                 + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")");
    487         // Deleting a Mailbox deletes associated Messages in all three tables
    488         db.execSQL(TRIGGER_MAILBOX_DELETE);
    489     }
    490 
    491     static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    492         try {
    493             db.execSQL("drop table " + Mailbox.TABLE_NAME);
    494         } catch (SQLException e) {
    495         }
    496         createMailboxTable(db);
    497     }
    498 
    499     static void createAttachmentTable(SQLiteDatabase db) {
    500         String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    501             + AttachmentColumns.FILENAME + " text, "
    502             + AttachmentColumns.MIME_TYPE + " text, "
    503             + AttachmentColumns.SIZE + " integer, "
    504             + AttachmentColumns.CONTENT_ID + " text, "
    505             + AttachmentColumns.CONTENT_URI + " text, "
    506             + AttachmentColumns.MESSAGE_KEY + " integer, "
    507             + AttachmentColumns.LOCATION + " text, "
    508             + AttachmentColumns.ENCODING + " text, "
    509             + AttachmentColumns.CONTENT + " text, "
    510             + AttachmentColumns.FLAGS + " integer, "
    511             + AttachmentColumns.CONTENT_BYTES + " blob"
    512             + ");";
    513         db.execSQL("create table " + Attachment.TABLE_NAME + s);
    514         db.execSQL(createIndex(Attachment.TABLE_NAME, AttachmentColumns.MESSAGE_KEY));
    515     }
    516 
    517     static void resetAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    518         try {
    519             db.execSQL("drop table " + Attachment.TABLE_NAME);
    520         } catch (SQLException e) {
    521         }
    522         createAttachmentTable(db);
    523     }
    524 
    525     static void createBodyTable(SQLiteDatabase db) {
    526         String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
    527             + BodyColumns.MESSAGE_KEY + " integer, "
    528             + BodyColumns.HTML_CONTENT + " text, "
    529             + BodyColumns.TEXT_CONTENT + " text, "
    530             + BodyColumns.HTML_REPLY + " text, "
    531             + BodyColumns.TEXT_REPLY + " text, "
    532             + BodyColumns.SOURCE_MESSAGE_KEY + " text, "
    533             + BodyColumns.INTRO_TEXT + " text"
    534             + ");";
    535         db.execSQL("create table " + Body.TABLE_NAME + s);
    536         db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY));
    537     }
    538 
    539     static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) {
    540         if (oldVersion < 5) {
    541             try {
    542                 db.execSQL("drop table " + Body.TABLE_NAME);
    543                 createBodyTable(db);
    544             } catch (SQLException e) {
    545             }
    546         } else if (oldVersion == 5) {
    547             try {
    548                 db.execSQL("alter table " + Body.TABLE_NAME
    549                         + " add " + BodyColumns.INTRO_TEXT + " text");
    550             } catch (SQLException e) {
    551                 // Shouldn't be needed unless we're debugging and interrupt the process
    552                 Log.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e);
    553             }
    554             oldVersion = 6;
    555         }
    556     }
    557 
    558     private SQLiteDatabase mDatabase;
    559     private SQLiteDatabase mBodyDatabase;
    560 
    561     public synchronized SQLiteDatabase getDatabase(Context context) {
    562         // Always return the cached database, if we've got one
    563         if (mDatabase != null) {
    564             return mDatabase;
    565         }
    566 
    567         // Whenever we create or re-cache the databases, make sure that we haven't lost one
    568         // to corruption
    569         checkDatabases();
    570 
    571         DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME);
    572         mDatabase = helper.getWritableDatabase();
    573         if (mDatabase != null) {
    574             mDatabase.setLockingEnabled(true);
    575             BodyDatabaseHelper bodyHelper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME);
    576             mBodyDatabase = bodyHelper.getWritableDatabase();
    577             if (mBodyDatabase != null) {
    578                 mBodyDatabase.setLockingEnabled(true);
    579                 String bodyFileName = mBodyDatabase.getPath();
    580                 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
    581             }
    582         }
    583 
    584         // Check for any orphaned Messages in the updated/deleted tables
    585         deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
    586         deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME);
    587 
    588         return mDatabase;
    589     }
    590 
    591     /*package*/ static SQLiteDatabase getReadableDatabase(Context context) {
    592         DatabaseHelper helper = new EmailProvider().new DatabaseHelper(context, DATABASE_NAME);
    593         return helper.getReadableDatabase();
    594     }
    595 
    596     /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) {
    597         if (database != null) {
    598             // We'll look at all of the items in the table; there won't be many typically
    599             Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
    600             // Usually, there will be nothing in these tables, so make a quick check
    601             try {
    602                 if (c.getCount() == 0) return;
    603                 ArrayList<Long> foundMailboxes = new ArrayList<Long>();
    604                 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
    605                 ArrayList<Long> deleteList = new ArrayList<Long>();
    606                 String[] bindArray = new String[1];
    607                 while (c.moveToNext()) {
    608                     // Get the mailbox key and see if we've already found this mailbox
    609                     // If so, we're fine
    610                     long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
    611                     // If we already know this mailbox doesn't exist, mark the message for deletion
    612                     if (notFoundMailboxes.contains(mailboxId)) {
    613                         deleteList.add(c.getLong(ORPHANS_ID));
    614                     // If we don't know about this mailbox, we'll try to find it
    615                     } else if (!foundMailboxes.contains(mailboxId)) {
    616                         bindArray[0] = Long.toString(mailboxId);
    617                         Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
    618                                 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
    619                         try {
    620                             // If it exists, we'll add it to the "found" mailboxes
    621                             if (boxCursor.moveToFirst()) {
    622                                 foundMailboxes.add(mailboxId);
    623                             // Otherwise, we'll add to "not found" and mark the message for deletion
    624                             } else {
    625                                 notFoundMailboxes.add(mailboxId);
    626                                 deleteList.add(c.getLong(ORPHANS_ID));
    627                             }
    628                         } finally {
    629                             boxCursor.close();
    630                         }
    631                     }
    632                 }
    633                 // Now, delete the orphan messages
    634                 for (long messageId: deleteList) {
    635                     bindArray[0] = Long.toString(messageId);
    636                     database.delete(tableName, WHERE_ID, bindArray);
    637                 }
    638             } finally {
    639                 c.close();
    640             }
    641         }
    642     }
    643 
    644     private class BodyDatabaseHelper extends SQLiteOpenHelper {
    645         BodyDatabaseHelper(Context context, String name) {
    646             super(context, name, null, BODY_DATABASE_VERSION);
    647         }
    648 
    649         @Override
    650         public void onCreate(SQLiteDatabase db) {
    651             Log.d(TAG, "Creating EmailProviderBody database");
    652             createBodyTable(db);
    653         }
    654 
    655         @Override
    656         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    657             upgradeBodyTable(db, oldVersion, newVersion);
    658         }
    659 
    660         @Override
    661         public void onOpen(SQLiteDatabase db) {
    662         }
    663     }
    664 
    665     private class DatabaseHelper extends SQLiteOpenHelper {
    666         Context mContext;
    667 
    668         DatabaseHelper(Context context, String name) {
    669             super(context, name, null, DATABASE_VERSION);
    670             mContext = context;
    671         }
    672 
    673         @Override
    674         public void onCreate(SQLiteDatabase db) {
    675             Log.d(TAG, "Creating EmailProvider database");
    676             // Create all tables here; each class has its own method
    677             createMessageTable(db);
    678             createAttachmentTable(db);
    679             createMailboxTable(db);
    680             createHostAuthTable(db);
    681             createAccountTable(db);
    682         }
    683 
    684         @Override
    685         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    686             // For versions prior to 5, delete all data
    687             // Versions >= 5 require that data be preserved!
    688             if (oldVersion < 5) {
    689                 android.accounts.Account[] accounts = AccountManager.get(mContext)
    690                         .getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    691                 for (android.accounts.Account account: accounts) {
    692                     AccountManager.get(mContext).removeAccount(account, null, null);
    693                 }
    694                 resetMessageTable(db, oldVersion, newVersion);
    695                 resetAttachmentTable(db, oldVersion, newVersion);
    696                 resetMailboxTable(db, oldVersion, newVersion);
    697                 resetHostAuthTable(db, oldVersion, newVersion);
    698                 resetAccountTable(db, oldVersion, newVersion);
    699                 return;
    700             }
    701             if (oldVersion == 5) {
    702                 // Message Tables: Add SyncColumns.SERVER_TIMESTAMP
    703                 try {
    704                     db.execSQL("alter table " + Message.TABLE_NAME
    705                             + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
    706                     db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
    707                             + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
    708                     db.execSQL("alter table " + Message.DELETED_TABLE_NAME
    709                             + " add column " + SyncColumns.SERVER_TIMESTAMP + " integer" + ";");
    710                 } catch (SQLException e) {
    711                     // Shouldn't be needed unless we're debugging and interrupt the process
    712                     Log.w(TAG, "Exception upgrading EmailProvider.db from v5 to v6", e);
    713                 }
    714                 oldVersion = 6;
    715             }
    716             if (oldVersion == 6) {
    717                 // Use the newer mailbox_delete trigger
    718                 db.execSQL("drop trigger mailbox_delete;");
    719                 db.execSQL(TRIGGER_MAILBOX_DELETE);
    720                 oldVersion = 7;
    721             }
    722             if (oldVersion == 7) {
    723                 // add the security (provisioning) column
    724                 try {
    725                     db.execSQL("alter table " + Account.TABLE_NAME
    726                             + " add column " + AccountColumns.SECURITY_FLAGS + " integer" + ";");
    727                 } catch (SQLException e) {
    728                     // Shouldn't be needed unless we're debugging and interrupt the process
    729                     Log.w(TAG, "Exception upgrading EmailProvider.db from 7 to 8 " + e);
    730                 }
    731                 oldVersion = 8;
    732             }
    733             if (oldVersion == 8) {
    734                 // accounts: add security sync key & user signature columns
    735                 try {
    736                     db.execSQL("alter table " + Account.TABLE_NAME
    737                             + " add column " + AccountColumns.SECURITY_SYNC_KEY + " text" + ";");
    738                     db.execSQL("alter table " + Account.TABLE_NAME
    739                             + " add column " + AccountColumns.SIGNATURE + " text" + ";");
    740                 } catch (SQLException e) {
    741                     // Shouldn't be needed unless we're debugging and interrupt the process
    742                     Log.w(TAG, "Exception upgrading EmailProvider.db from 8 to 9 " + e);
    743                 }
    744                 oldVersion = 9;
    745             }
    746             if (oldVersion == 9) {
    747                 // Message: add meeting info column into Message tables
    748                 try {
    749                     db.execSQL("alter table " + Message.TABLE_NAME
    750                             + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
    751                     db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
    752                             + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
    753                     db.execSQL("alter table " + Message.DELETED_TABLE_NAME
    754                             + " add column " + MessageColumns.MEETING_INFO + " text" + ";");
    755                 } catch (SQLException e) {
    756                     // Shouldn't be needed unless we're debugging and interrupt the process
    757                     Log.w(TAG, "Exception upgrading EmailProvider.db from 9 to 10 " + e);
    758                 }
    759                 oldVersion = 10;
    760             }
    761             if (oldVersion == 10) {
    762                 // Attachment: add content and flags columns
    763                 try {
    764                     db.execSQL("alter table " + Attachment.TABLE_NAME
    765                             + " add column " + AttachmentColumns.CONTENT + " text" + ";");
    766                     db.execSQL("alter table " + Attachment.TABLE_NAME
    767                             + " add column " + AttachmentColumns.FLAGS + " integer" + ";");
    768                 } catch (SQLException e) {
    769                     // Shouldn't be needed unless we're debugging and interrupt the process
    770                     Log.w(TAG, "Exception upgrading EmailProvider.db from 10 to 11 " + e);
    771                 }
    772                 oldVersion = 11;
    773             }
    774             if (oldVersion == 11) {
    775                 // Attachment: add content_bytes
    776                 try {
    777                     db.execSQL("alter table " + Attachment.TABLE_NAME
    778                             + " add column " + AttachmentColumns.CONTENT_BYTES + " blob" + ";");
    779                 } catch (SQLException e) {
    780                     // Shouldn't be needed unless we're debugging and interrupt the process
    781                     Log.w(TAG, "Exception upgrading EmailProvider.db from 11 to 12 " + e);
    782                 }
    783                 oldVersion = 12;
    784             }
    785         }
    786 
    787         @Override
    788         public void onOpen(SQLiteDatabase db) {
    789         }
    790     }
    791 
    792     @Override
    793     public int delete(Uri uri, String selection, String[] selectionArgs) {
    794         final int match = sURIMatcher.match(uri);
    795         Context context = getContext();
    796         // Pick the correct database for this operation
    797         // If we're in a transaction already (which would happen during applyBatch), then the
    798         // body database is already attached to the email database and any attempt to use the
    799         // body database directly will result in a SQLiteException (the database is locked)
    800         SQLiteDatabase db = getDatabase(context);
    801         int table = match >> BASE_SHIFT;
    802         String id = "0";
    803         boolean messageDeletion = false;
    804 
    805         if (Email.LOGD) {
    806             Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match);
    807         }
    808 
    809         int result = -1;
    810 
    811         try {
    812             switch (match) {
    813                 // These are cases in which one or more Messages might get deleted, either by
    814                 // cascade or explicitly
    815                 case MAILBOX_ID:
    816                 case MAILBOX:
    817                 case ACCOUNT_ID:
    818                 case ACCOUNT:
    819                 case MESSAGE:
    820                 case SYNCED_MESSAGE_ID:
    821                 case MESSAGE_ID:
    822                     // Handle lost Body records here, since this cannot be done in a trigger
    823                     // The process is:
    824                     //  1) Begin a transaction, ensuring that both databases are affected atomically
    825                     //  2) Do the requested deletion, with cascading deletions handled in triggers
    826                     //  3) End the transaction, committing all changes atomically
    827                     //
    828                     // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
    829 
    830                     messageDeletion = true;
    831                     db.beginTransaction();
    832                     break;
    833             }
    834             switch (match) {
    835                 case BODY_ID:
    836                 case DELETED_MESSAGE_ID:
    837                 case SYNCED_MESSAGE_ID:
    838                 case MESSAGE_ID:
    839                 case UPDATED_MESSAGE_ID:
    840                 case ATTACHMENT_ID:
    841                 case MAILBOX_ID:
    842                 case ACCOUNT_ID:
    843                 case HOSTAUTH_ID:
    844                     id = uri.getPathSegments().get(1);
    845                     if (match == SYNCED_MESSAGE_ID) {
    846                         // For synced messages, first copy the old message to the deleted table and
    847                         // delete it from the updated table (in case it was updated first)
    848                         // Note that this is all within a transaction, for atomicity
    849                         db.execSQL(DELETED_MESSAGE_INSERT + id);
    850                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
    851                     }
    852                     result = db.delete(TABLE_NAMES[table], whereWithId(id, selection),
    853                             selectionArgs);
    854                     break;
    855                 case ATTACHMENTS_MESSAGE_ID:
    856                     // All attachments for the given message
    857                     id = uri.getPathSegments().get(2);
    858                     result = db.delete(TABLE_NAMES[table],
    859                             whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
    860                     break;
    861 
    862                 case BODY:
    863                 case MESSAGE:
    864                 case DELETED_MESSAGE:
    865                 case UPDATED_MESSAGE:
    866                 case ATTACHMENT:
    867                 case MAILBOX:
    868                 case ACCOUNT:
    869                 case HOSTAUTH:
    870                     result = db.delete(TABLE_NAMES[table], selection, selectionArgs);
    871                     break;
    872 
    873                 default:
    874                     throw new IllegalArgumentException("Unknown URI " + uri);
    875             }
    876             if (messageDeletion) {
    877                 if (match == MESSAGE_ID) {
    878                     // Delete the Body record associated with the deleted message
    879                     db.execSQL(DELETE_BODY + id);
    880                 } else {
    881                     // Delete any orphaned Body records
    882                     db.execSQL(DELETE_ORPHAN_BODIES);
    883                 }
    884                 db.setTransactionSuccessful();
    885             }
    886         } catch (SQLiteException e) {
    887             checkDatabases();
    888             throw e;
    889         } finally {
    890             if (messageDeletion) {
    891                 db.endTransaction();
    892             }
    893         }
    894         getContext().getContentResolver().notifyChange(uri, null);
    895         return result;
    896     }
    897 
    898     @Override
    899     // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
    900     public String getType(Uri uri) {
    901         int match = sURIMatcher.match(uri);
    902         switch (match) {
    903             case BODY_ID:
    904                 return "vnd.android.cursor.item/email-body";
    905             case BODY:
    906                 return "vnd.android.cursor.dir/email-message";
    907             case UPDATED_MESSAGE_ID:
    908             case MESSAGE_ID:
    909                 return "vnd.android.cursor.item/email-message";
    910             case MAILBOX_MESSAGES:
    911             case UPDATED_MESSAGE:
    912             case MESSAGE:
    913                 return "vnd.android.cursor.dir/email-message";
    914             case ACCOUNT_MAILBOXES:
    915             case MAILBOX:
    916                 return "vnd.android.cursor.dir/email-mailbox";
    917             case MAILBOX_ID:
    918                 return "vnd.android.cursor.item/email-mailbox";
    919             case ACCOUNT:
    920                 return "vnd.android.cursor.dir/email-account";
    921             case ACCOUNT_ID:
    922                 return "vnd.android.cursor.item/email-account";
    923             case ATTACHMENTS_MESSAGE_ID:
    924             case ATTACHMENT:
    925                 return "vnd.android.cursor.dir/email-attachment";
    926             case ATTACHMENT_ID:
    927                 return "vnd.android.cursor.item/email-attachment";
    928             case HOSTAUTH:
    929                 return "vnd.android.cursor.dir/email-hostauth";
    930             case HOSTAUTH_ID:
    931                 return "vnd.android.cursor.item/email-hostauth";
    932             default:
    933                 throw new IllegalArgumentException("Unknown URI " + uri);
    934         }
    935     }
    936 
    937     @Override
    938     public Uri insert(Uri uri, ContentValues values) {
    939         int match = sURIMatcher.match(uri);
    940         Context context = getContext();
    941         // See the comment at delete(), above
    942         SQLiteDatabase db = getDatabase(context);
    943         int table = match >> BASE_SHIFT;
    944         long id;
    945 
    946         if (Email.LOGD) {
    947             Log.v(TAG, "EmailProvider.insert: uri=" + uri + ", match is " + match);
    948         }
    949 
    950         Uri resultUri = null;
    951 
    952         try {
    953             switch (match) {
    954                 case UPDATED_MESSAGE:
    955                 case DELETED_MESSAGE:
    956                 case BODY:
    957                 case MESSAGE:
    958                 case ATTACHMENT:
    959                 case MAILBOX:
    960                 case ACCOUNT:
    961                 case HOSTAUTH:
    962                     id = db.insert(TABLE_NAMES[table], "foo", values);
    963                     resultUri = ContentUris.withAppendedId(uri, id);
    964                     // Clients shouldn't normally be adding rows to these tables, as they are
    965                     // maintained by triggers.  However, we need to be able to do this for unit
    966                     // testing, so we allow the insert and then throw the same exception that we
    967                     // would if this weren't allowed.
    968                     if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) {
    969                         throw new IllegalArgumentException("Unknown URL " + uri);
    970                     }
    971                     break;
    972                 case MAILBOX_ID:
    973                     // This implies adding a message to a mailbox
    974                     // Hmm, a problem here is that we can't link the account as well, so it must be
    975                     // already in the values...
    976                     id = Long.parseLong(uri.getPathSegments().get(1));
    977                     values.put(MessageColumns.MAILBOX_KEY, id);
    978                     resultUri = insert(Message.CONTENT_URI, values);
    979                     break;
    980                 case MESSAGE_ID:
    981                     // This implies adding an attachment to a message.
    982                     id = Long.parseLong(uri.getPathSegments().get(1));
    983                     values.put(AttachmentColumns.MESSAGE_KEY, id);
    984                     resultUri = insert(Attachment.CONTENT_URI, values);
    985                     break;
    986                 case ACCOUNT_ID:
    987                     // This implies adding a mailbox to an account.
    988                     id = Long.parseLong(uri.getPathSegments().get(1));
    989                     values.put(MailboxColumns.ACCOUNT_KEY, id);
    990                     resultUri = insert(Mailbox.CONTENT_URI, values);
    991                     break;
    992                 case ATTACHMENTS_MESSAGE_ID:
    993                     id = db.insert(TABLE_NAMES[table], "foo", values);
    994                     resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
    995                     break;
    996                 default:
    997                     throw new IllegalArgumentException("Unknown URL " + uri);
    998             }
    999         } catch (SQLiteException e) {
   1000             checkDatabases();
   1001             throw e;
   1002         }
   1003 
   1004         // Notify with the base uri, not the new uri (nobody is watching a new record)
   1005         getContext().getContentResolver().notifyChange(uri, null);
   1006         return resultUri;
   1007     }
   1008 
   1009     @Override
   1010     public boolean onCreate() {
   1011         checkDatabases();
   1012         return false;
   1013     }
   1014 
   1015     /**
   1016      * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
   1017      * always be in sync (i.e. there are two database or NO databases).  This code will delete
   1018      * any "orphan" database, so that both will be created together.  Note that an "orphan" database
   1019      * will exist after either of the individual databases is deleted due to data corruption.
   1020      */
   1021     public void checkDatabases() {
   1022         // Uncache the databases
   1023         if (mDatabase != null) {
   1024             mDatabase = null;
   1025         }
   1026         if (mBodyDatabase != null) {
   1027             mBodyDatabase = null;
   1028         }
   1029         // Look for orphans, and delete as necessary; these must always be in sync
   1030         File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
   1031         File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
   1032 
   1033         // TODO Make sure attachments are deleted
   1034         if (databaseFile.exists() && !bodyFile.exists()) {
   1035             Log.w(TAG, "Deleting orphaned EmailProvider database...");
   1036             databaseFile.delete();
   1037         } else if (bodyFile.exists() && !databaseFile.exists()) {
   1038             Log.w(TAG, "Deleting orphaned EmailProviderBody database...");
   1039             bodyFile.delete();
   1040         }
   1041     }
   1042 
   1043     @Override
   1044     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   1045             String sortOrder) {
   1046         Cursor c = null;
   1047         Uri notificationUri = EmailContent.CONTENT_URI;
   1048         int match = sURIMatcher.match(uri);
   1049         Context context = getContext();
   1050         // See the comment at delete(), above
   1051         SQLiteDatabase db = getDatabase(context);
   1052         int table = match >> BASE_SHIFT;
   1053         String id;
   1054 
   1055         if (Email.LOGD) {
   1056             Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match);
   1057         }
   1058 
   1059         try {
   1060             switch (match) {
   1061                 case BODY:
   1062                 case MESSAGE:
   1063                 case UPDATED_MESSAGE:
   1064                 case DELETED_MESSAGE:
   1065                 case ATTACHMENT:
   1066                 case MAILBOX:
   1067                 case ACCOUNT:
   1068                 case HOSTAUTH:
   1069                     c = db.query(TABLE_NAMES[table], projection,
   1070                             selection, selectionArgs, null, null, sortOrder);
   1071                     break;
   1072                 case BODY_ID:
   1073                 case MESSAGE_ID:
   1074                 case DELETED_MESSAGE_ID:
   1075                 case UPDATED_MESSAGE_ID:
   1076                 case ATTACHMENT_ID:
   1077                 case MAILBOX_ID:
   1078                 case ACCOUNT_ID:
   1079                 case HOSTAUTH_ID:
   1080                     id = uri.getPathSegments().get(1);
   1081                     c = db.query(TABLE_NAMES[table], projection,
   1082                             whereWithId(id, selection), selectionArgs, null, null, sortOrder);
   1083                     break;
   1084                 case ATTACHMENTS_MESSAGE_ID:
   1085                     // All attachments for the given message
   1086                     id = uri.getPathSegments().get(2);
   1087                     c = db.query(Attachment.TABLE_NAME, projection,
   1088                             whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
   1089                             selectionArgs, null, null, sortOrder);
   1090                     break;
   1091                 default:
   1092                     throw new IllegalArgumentException("Unknown URI " + uri);
   1093             }
   1094         } catch (SQLiteException e) {
   1095             checkDatabases();
   1096             throw e;
   1097         }
   1098 
   1099         if ((c != null) && !isTemporary()) {
   1100             c.setNotificationUri(getContext().getContentResolver(), notificationUri);
   1101         }
   1102         return c;
   1103     }
   1104 
   1105     private String whereWithId(String id, String selection) {
   1106         StringBuilder sb = new StringBuilder(256);
   1107         sb.append("_id=");
   1108         sb.append(id);
   1109         if (selection != null) {
   1110             sb.append(" AND (");
   1111             sb.append(selection);
   1112             sb.append(')');
   1113         }
   1114         return sb.toString();
   1115     }
   1116 
   1117     /**
   1118      * Combine a locally-generated selection with a user-provided selection
   1119      *
   1120      * This introduces risk that the local selection might insert incorrect chars
   1121      * into the SQL, so use caution.
   1122      *
   1123      * @param where locally-generated selection, must not be null
   1124      * @param selection user-provided selection, may be null
   1125      * @return a single selection string
   1126      */
   1127     private String whereWith(String where, String selection) {
   1128         if (selection == null) {
   1129             return where;
   1130         }
   1131         StringBuilder sb = new StringBuilder(where);
   1132         sb.append(" AND (");
   1133         sb.append(selection);
   1134         sb.append(')');
   1135 
   1136         return sb.toString();
   1137     }
   1138 
   1139     @Override
   1140     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   1141         int match = sURIMatcher.match(uri);
   1142         Context context = getContext();
   1143         // See the comment at delete(), above
   1144         SQLiteDatabase db = getDatabase(context);
   1145         int table = match >> BASE_SHIFT;
   1146         int result;
   1147 
   1148         if (Email.LOGD) {
   1149             Log.v(TAG, "EmailProvider.update: uri=" + uri + ", match is " + match);
   1150         }
   1151 
   1152         // We do NOT allow setting of unreadCount via the provider
   1153         // This column is maintained via triggers
   1154         if (match == MAILBOX_ID || match == MAILBOX) {
   1155             values.remove(MailboxColumns.UNREAD_COUNT);
   1156         }
   1157 
   1158         // Handle this special case the fastest possible way
   1159         if (uri == INTEGRITY_CHECK_URI) {
   1160             checkDatabases();
   1161             return 0;
   1162         }
   1163 
   1164         String id;
   1165         try {
   1166             switch (match) {
   1167                 case MAILBOX_ID_ADD_TO_FIELD:
   1168                 case ACCOUNT_ID_ADD_TO_FIELD:
   1169                     db.beginTransaction();
   1170                     id = uri.getPathSegments().get(1);
   1171                     String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME);
   1172                     Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME);
   1173                     if (field == null || add == null) {
   1174                         throw new IllegalArgumentException("No field/add specified " + uri);
   1175                     }
   1176                     Cursor c = db.query(TABLE_NAMES[table],
   1177                             new String[] {EmailContent.RECORD_ID, field},
   1178                             whereWithId(id, selection),
   1179                             selectionArgs, null, null, null);
   1180                     try {
   1181                         result = 0;
   1182                         ContentValues cv = new ContentValues();
   1183                         String[] bind = new String[1];
   1184                         while (c.moveToNext()) {
   1185                             bind[0] = c.getString(0);
   1186                             long value = c.getLong(1) + add;
   1187                             cv.put(field, value);
   1188                             result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind);
   1189                         }
   1190                     } finally {
   1191                         c.close();
   1192                     }
   1193                     db.setTransactionSuccessful();
   1194                     db.endTransaction();
   1195                     break;
   1196                 case BODY_ID:
   1197                 case MESSAGE_ID:
   1198                 case SYNCED_MESSAGE_ID:
   1199                 case UPDATED_MESSAGE_ID:
   1200                 case ATTACHMENT_ID:
   1201                 case MAILBOX_ID:
   1202                 case ACCOUNT_ID:
   1203                 case HOSTAUTH_ID:
   1204                     id = uri.getPathSegments().get(1);
   1205                     if (match == SYNCED_MESSAGE_ID) {
   1206                         // For synced messages, first copy the old message to the updated table
   1207                         // Note the insert or ignore semantics, guaranteeing that only the first
   1208                         // update will be reflected in the updated message table; therefore this row
   1209                         // will always have the "original" data
   1210                         db.execSQL(UPDATED_MESSAGE_INSERT + id);
   1211                     } else if (match == MESSAGE_ID) {
   1212                         db.execSQL(UPDATED_MESSAGE_DELETE + id);
   1213                     }
   1214                     result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection),
   1215                             selectionArgs);
   1216                     break;
   1217                 case BODY:
   1218                 case MESSAGE:
   1219                 case UPDATED_MESSAGE:
   1220                 case ATTACHMENT:
   1221                 case MAILBOX:
   1222                 case ACCOUNT:
   1223                 case HOSTAUTH:
   1224                     result = db.update(TABLE_NAMES[table], values, selection, selectionArgs);
   1225                     break;
   1226                 default:
   1227                     throw new IllegalArgumentException("Unknown URI " + uri);
   1228             }
   1229         } catch (SQLiteException e) {
   1230             checkDatabases();
   1231             throw e;
   1232         }
   1233 
   1234         getContext().getContentResolver().notifyChange(uri, null);
   1235         return result;
   1236     }
   1237 
   1238     /* (non-Javadoc)
   1239      * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation)
   1240      */
   1241     @Override
   1242     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   1243             throws OperationApplicationException {
   1244         Context context = getContext();
   1245         SQLiteDatabase db = getDatabase(context);
   1246         db.beginTransaction();
   1247         try {
   1248             ContentProviderResult[] results = super.applyBatch(operations);
   1249             db.setTransactionSuccessful();
   1250             return results;
   1251         } finally {
   1252             db.endTransaction();
   1253         }
   1254     }
   1255 }
   1256