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