EmailProvider.java revision 53093871c492d03947c494f17e2463b27345e083
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;
33
34import android.content.ContentProvider;
35import android.content.ContentProviderOperation;
36import android.content.ContentProviderResult;
37import android.content.ContentUris;
38import android.content.ContentValues;
39import android.content.Context;
40import android.content.OperationApplicationException;
41import android.content.UriMatcher;
42import android.database.Cursor;
43import android.database.SQLException;
44import android.database.sqlite.SQLiteDatabase;
45import android.database.sqlite.SQLiteOpenHelper;
46import android.net.Uri;
47import android.util.Config;
48import android.util.Log;
49
50import java.util.ArrayList;
51
52public class EmailProvider extends ContentProvider {
53
54    private static final String TAG = "EmailProvider";
55
56    static final String DATABASE_NAME = "EmailProvider.db";
57    static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
58
59    // In these early versions, updating the database version will cause all tables to be deleted
60    // Obviously, we'll handle upgrades differently once things are a bit stable
61    // version 15: changed Address.pack() format.
62    // version 16: added protocolVersion column to Account
63    public static final int DATABASE_VERSION = 16;
64    public static final int BODY_DATABASE_VERSION = 1;
65
66    public static final String EMAIL_AUTHORITY = "com.android.email.provider";
67
68    private static final int ACCOUNT_BASE = 0;
69    private static final int ACCOUNT = ACCOUNT_BASE;
70    private static final int ACCOUNT_MAILBOXES = ACCOUNT_BASE + 1;
71    private static final int ACCOUNT_ID = ACCOUNT_BASE + 2;
72
73    private static final int MAILBOX_BASE = 0x1000;
74    private static final int MAILBOX = MAILBOX_BASE;
75    private static final int MAILBOX_MESSAGES = MAILBOX_BASE + 1;
76    private static final int MAILBOX_ID = MAILBOX_BASE + 2;
77
78    private static final int MESSAGE_BASE = 0x2000;
79    private static final int MESSAGE = MESSAGE_BASE;
80    private static final int MESSAGE_ID = MESSAGE_BASE + 1;
81    private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
82
83    private static final int ATTACHMENT_BASE = 0x3000;
84    private static final int ATTACHMENT = ATTACHMENT_BASE;
85    private static final int ATTACHMENT_CONTENT = ATTACHMENT_BASE + 1;
86    private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 2;
87    private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 3;
88
89    private static final int HOSTAUTH_BASE = 0x4000;
90    private static final int HOSTAUTH = HOSTAUTH_BASE;
91    private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
92
93    private static final int UPDATED_MESSAGE_BASE = 0x5000;
94    private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
95    private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
96
97    private static final int DELETED_MESSAGE_BASE = 0x6000;
98    private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
99    private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
100    private static final int DELETED_MESSAGE_MAILBOX = DELETED_MESSAGE_BASE + 2;
101
102    // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS
103    private static final int LAST_EMAIL_PROVIDER_DB_BASE = DELETED_MESSAGE_BASE;
104
105    // DO NOT CHANGE BODY_BASE!!
106    private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000;
107    private static final int BODY = BODY_BASE;
108    private static final int BODY_ID = BODY_BASE + 1;
109    private static final int BODY_MESSAGE_ID = BODY_BASE + 2;
110    private static final int BODY_HTML = BODY_BASE + 3;
111    private static final int BODY_TEXT = BODY_BASE + 4;
112
113
114    private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
115
116    private static final String[] TABLE_NAMES = {
117        EmailContent.Account.TABLE_NAME,
118        EmailContent.Mailbox.TABLE_NAME,
119        EmailContent.Message.TABLE_NAME,
120        EmailContent.Attachment.TABLE_NAME,
121        EmailContent.HostAuth.TABLE_NAME,
122        EmailContent.Message.UPDATED_TABLE_NAME,
123        EmailContent.Message.DELETED_TABLE_NAME,
124        EmailContent.Body.TABLE_NAME
125    };
126
127    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
128
129    /**
130     * Let's only generate these SQL strings once, as they are used frequently
131     * Note that this isn't relevant for table creation strings, since they are used only once
132     */
133    private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
134        Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
135        EmailContent.RECORD_ID + '=';
136
137    private static final String UPDATED_MESSAGE_DELETE = "delete from " +
138        Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '=';
139
140    private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
141        Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
142        EmailContent.RECORD_ID + '=';
143
144    private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
145        " where " + EmailContent.RECORD_ID + " in " + "(select " + EmailContent.RECORD_ID +
146        " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " +
147        Message.TABLE_NAME + ')';
148
149    private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
150        " where " + EmailContent.RECORD_ID + '=';
151
152    static {
153        // Email URI matching table
154        UriMatcher matcher = sURIMatcher;
155
156        // All accounts
157        matcher.addURI(EMAIL_AUTHORITY, "account", ACCOUNT);
158        // A specific account
159        // insert into this URI causes a mailbox to be added to the account
160        matcher.addURI(EMAIL_AUTHORITY, "account/#", ACCOUNT_ID);
161        // The mailboxes in a specific account
162        matcher.addURI(EMAIL_AUTHORITY, "account/#/mailbox", ACCOUNT_MAILBOXES);
163
164        // All mailboxes
165        matcher.addURI(EMAIL_AUTHORITY, "mailbox", MAILBOX);
166        // A specific mailbox
167        // insert into this URI causes a message to be added to the mailbox
168        // ** NOTE For now, the accountKey must be set manually in the values!
169        matcher.addURI(EMAIL_AUTHORITY, "mailbox/#", MAILBOX_ID);
170        // The messages in a specific mailbox
171        matcher.addURI(EMAIL_AUTHORITY, "mailbox/#/message", MAILBOX_MESSAGES);
172
173        // All messages
174        matcher.addURI(EMAIL_AUTHORITY, "message", MESSAGE);
175        // A specific message
176        // insert into this URI causes an attachment to be added to the message
177        matcher.addURI(EMAIL_AUTHORITY, "message/#", MESSAGE_ID);
178
179        // A specific attachment
180        matcher.addURI(EMAIL_AUTHORITY, "attachment", ATTACHMENT);
181        // A specific attachment (the header information)
182        matcher.addURI(EMAIL_AUTHORITY, "attachment/#", ATTACHMENT_ID);
183        // The content for a specific attachment
184        // NOT IMPLEMENTED
185        matcher.addURI(EMAIL_AUTHORITY, "attachment/content/*", ATTACHMENT_CONTENT);
186        // The attachments of a specific message (query only) (insert & delete TBD)
187        matcher.addURI(EMAIL_AUTHORITY, "attachment/message/#", ATTACHMENTS_MESSAGE_ID);
188
189        // All mail bodies
190        matcher.addURI(EMAIL_AUTHORITY, "body", BODY);
191        // A specific mail body
192        matcher.addURI(EMAIL_AUTHORITY, "body/#", BODY_ID);
193        // The body for a specific message
194        matcher.addURI(EMAIL_AUTHORITY, "body/message/#", BODY_MESSAGE_ID);
195        // The HTML part of a specific mail body
196        matcher.addURI(EMAIL_AUTHORITY, "body/#/html", BODY_HTML);
197        // The plain text part of a specific mail body
198        matcher.addURI(EMAIL_AUTHORITY, "body/#/text", BODY_TEXT);
199
200        // All hostauth records
201        matcher.addURI(EMAIL_AUTHORITY, "hostauth", HOSTAUTH);
202        // A specific hostauth
203        matcher.addURI(EMAIL_AUTHORITY, "hostauth/#", HOSTAUTH_ID);
204
205        /**
206         * THIS URI HAS SPECIAL SEMANTICS
207         * ITS USE IS INDENTED FOR THE UI APPLICATION TO MARK CHANGES THAT NEED TO BE SYNCED BACK
208         * TO A SERVER VIA A SYNC ADAPTER
209         */
210        matcher.addURI(EMAIL_AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
211
212        /**
213         * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
214         * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
215         * BY THE UI APPLICATION
216         */
217        // All deleted messages
218        matcher.addURI(EMAIL_AUTHORITY, "deletedMessage", DELETED_MESSAGE);
219        // A specific deleted message
220        matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
221        // All deleted messages from a specific mailbox
222        // NOT IMPLEMENTED; do we need this as a convenience?
223        matcher.addURI(EMAIL_AUTHORITY, "deletedMessage/mailbox/#", DELETED_MESSAGE_MAILBOX);
224
225        // All updated messages
226        matcher.addURI(EMAIL_AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
227        // A specific updated message
228        matcher.addURI(EMAIL_AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
229    }
230
231    static void createMessageTable(SQLiteDatabase db) {
232        String messageColumns = MessageColumns.DISPLAY_NAME + " text, "
233            + MessageColumns.TIMESTAMP + " integer, "
234            + MessageColumns.SUBJECT + " text, "
235            + MessageColumns.PREVIEW + " text, "
236            + MessageColumns.FLAG_READ + " integer, "
237            + MessageColumns.FLAG_LOADED + " integer, "
238            + MessageColumns.FLAG_FAVORITE + " integer, "
239            + MessageColumns.FLAG_ATTACHMENT + " integer, "
240            + MessageColumns.FLAGS + " integer, "
241            + MessageColumns.TEXT_INFO + " text, "
242            + MessageColumns.HTML_INFO + " text, "
243            + MessageColumns.CLIENT_ID + " integer, "
244            + MessageColumns.MESSAGE_ID + " text, "
245            + MessageColumns.THREAD_ID + " text, "
246            + MessageColumns.MAILBOX_KEY + " integer, "
247            + MessageColumns.ACCOUNT_KEY + " integer, "
248            + MessageColumns.REFERENCE_KEY + " integer, "
249            + MessageColumns.SENDER_LIST + " text, "
250            + MessageColumns.FROM_LIST + " text, "
251            + MessageColumns.TO_LIST + " text, "
252            + MessageColumns.CC_LIST + " text, "
253            + MessageColumns.BCC_LIST + " text, "
254            + MessageColumns.REPLY_TO_LIST + " text"
255            + ");";
256
257        // This String and the following String MUST have the same columns, except for the type
258        // of those columns!
259        String createString = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
260            + SyncColumns.ACCOUNT_KEY + " integer, "
261            + SyncColumns.SERVER_ID + " integer, "
262            + SyncColumns.SERVER_VERSION + " integer, "
263            + SyncColumns.DATA + " text, "
264            + SyncColumns.DIRTY_COUNT + " integer, "
265            + messageColumns;
266
267        // For the updated and deleted tables, the id is assigned, but we do want to keep track
268        // of the ORDER of updates using an autoincrement primary key.  We use the DATA column
269        // at this point; it has no other function
270        String altCreateString = " (" + EmailContent.RECORD_ID + " integer unique, "
271            + SyncColumns.ACCOUNT_KEY + " integer, "
272            + SyncColumns.SERVER_ID + " integer, "
273            + SyncColumns.SERVER_VERSION + " integer, "
274            + SyncColumns.DATA + " integer primary key autoincrement, "
275            + SyncColumns.DIRTY_COUNT + " integer, "
276            + messageColumns;
277
278        // The three tables have the same schema
279        db.execSQL("create table " + Message.TABLE_NAME + createString);
280        db.execSQL("create table " + Message.UPDATED_TABLE_NAME + altCreateString);
281        db.execSQL("create table " + Message.DELETED_TABLE_NAME + altCreateString);
282
283        // For now, indices only on the Message table
284        db.execSQL("create index message_" + MessageColumns.TIMESTAMP
285                + " on " + Message.TABLE_NAME + " (" + MessageColumns.TIMESTAMP + ");");
286        db.execSQL("create index message_" + MessageColumns.FLAG_READ
287                + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_READ + ");");
288        db.execSQL("create index message_" + MessageColumns.FLAG_LOADED
289                + " on " + Message.TABLE_NAME + " (" + MessageColumns.FLAG_LOADED + ");");
290        db.execSQL("create index message_" + MessageColumns.MAILBOX_KEY
291                + " on " + Message.TABLE_NAME + " (" + MessageColumns.MAILBOX_KEY + ");");
292        db.execSQL("create index message_" + SyncColumns.SERVER_ID
293                + " on " + Message.TABLE_NAME + " (" + SyncColumns.SERVER_ID + ");");
294
295        // Deleting a Message deletes all associated Attachments
296        // Deleting the associated Body cannot be done in a trigger, because the Body is stored
297        // in a separate database, and trigger cannot operate on attached databases.
298        db.execSQL("create trigger message_delete before delete on " + Message.TABLE_NAME +
299                " begin delete from " + Attachment.TABLE_NAME +
300                "  where " + AttachmentColumns.MESSAGE_KEY + "=old." + EmailContent.RECORD_ID +
301        "; end");
302    }
303
304    static void upgradeMessageTable(SQLiteDatabase db, int oldVersion, int newVersion) {
305        try {
306            db.execSQL("drop table " + Message.TABLE_NAME);
307            db.execSQL("drop table " + Message.UPDATED_TABLE_NAME);
308            db.execSQL("drop table " + Message.DELETED_TABLE_NAME);
309        } catch (SQLException e) {
310        }
311        createMessageTable(db);
312    }
313
314    static void createAccountTable(SQLiteDatabase db) {
315        String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
316            + AccountColumns.DISPLAY_NAME + " text, "
317            + AccountColumns.EMAIL_ADDRESS + " text, "
318            + AccountColumns.SYNC_KEY + " text, "
319            + AccountColumns.SYNC_LOOKBACK + " integer, "
320            + AccountColumns.SYNC_FREQUENCY + " text, "
321            + AccountColumns.HOST_AUTH_KEY_RECV + " integer, "
322            + AccountColumns.HOST_AUTH_KEY_SEND + " integer, "
323            + AccountColumns.FLAGS + " integer, "
324            + AccountColumns.IS_DEFAULT + " integer, "
325            + AccountColumns.COMPATIBILITY_UUID + " text, "
326            + AccountColumns.SENDER_NAME + " text, "
327            + AccountColumns.RINGTONE_URI + " text, "
328            + AccountColumns.PROTOCOL_VERSION + " text"
329            + ");";
330        db.execSQL("create table " + Account.TABLE_NAME + s);
331        // Deleting an account deletes associated Mailboxes and HostAuth's
332        db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME +
333                " begin delete from " + Mailbox.TABLE_NAME +
334                " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID +
335                "; delete from " + HostAuth.TABLE_NAME +
336                " where " + HostAuthColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID +
337        "; end");
338    }
339
340    static void upgradeAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) {
341        try {
342            db.execSQL("drop table " +  Account.TABLE_NAME);
343        } catch (SQLException e) {
344        }
345        createAccountTable(db);
346    }
347
348    static void createHostAuthTable(SQLiteDatabase db) {
349        String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
350            + HostAuthColumns.PROTOCOL + " text, "
351            + HostAuthColumns.ADDRESS + " text, "
352            + HostAuthColumns.PORT + " integer, "
353            + HostAuthColumns.FLAGS + " integer, "
354            + HostAuthColumns.LOGIN + " text, "
355            + HostAuthColumns.PASSWORD + " text, "
356            + HostAuthColumns.DOMAIN + " text, "
357            + HostAuthColumns.ACCOUNT_KEY + " integer"
358            + ");";
359        db.execSQL("create table " + HostAuth.TABLE_NAME + s);
360    }
361
362    static void upgradeHostAuthTable(SQLiteDatabase db, int oldVersion, int newVersion) {
363        try {
364            db.execSQL("drop table " + HostAuth.TABLE_NAME);
365        } catch (SQLException e) {
366        }
367        createHostAuthTable(db);
368    }
369
370    static void createMailboxTable(SQLiteDatabase db) {
371        String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
372            + MailboxColumns.DISPLAY_NAME + " text, "
373            + MailboxColumns.SERVER_ID + " text, "
374            + MailboxColumns.PARENT_SERVER_ID + " text, "
375            + MailboxColumns.ACCOUNT_KEY + " integer, "
376            + MailboxColumns.TYPE + " integer, "
377            + MailboxColumns.DELIMITER + " integer, "
378            + MailboxColumns.SYNC_KEY + " text, "
379            + MailboxColumns.SYNC_LOOKBACK + " integer, "
380            + MailboxColumns.SYNC_FREQUENCY+ " integer, "
381            + MailboxColumns.SYNC_TIME + " integer, "
382            + MailboxColumns.UNREAD_COUNT + " integer, "
383            + MailboxColumns.FLAG_VISIBLE + " integer, "
384            + MailboxColumns.FLAGS + " integer, "
385            + MailboxColumns.VISIBLE_LIMIT + " integer"
386            + ");";
387        db.execSQL("create table " + Mailbox.TABLE_NAME + s);
388        db.execSQL("create index mailbox_" + MailboxColumns.SERVER_ID
389                + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")");
390        db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY
391                + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")");
392        // Deleting a Mailbox deletes associated Messages
393        db.execSQL("create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME +
394                " begin delete from " + Message.TABLE_NAME +
395                "  where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
396                "; end");
397    }
398
399    static void upgradeMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) {
400        try {
401            db.execSQL("drop table " + Mailbox.TABLE_NAME);
402        } catch (SQLException e) {
403        }
404        createMailboxTable(db);
405    }
406
407    static void createAttachmentTable(SQLiteDatabase db) {
408        String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
409            + AttachmentColumns.FILENAME + " text, "
410            + AttachmentColumns.MIME_TYPE + " text, "
411            + AttachmentColumns.SIZE + " integer, "
412            + AttachmentColumns.CONTENT_ID + " text, "
413            + AttachmentColumns.CONTENT_URI + " text, "
414            + AttachmentColumns.MESSAGE_KEY + " integer, "
415            + AttachmentColumns.LOCATION + " text, "
416            + AttachmentColumns.ENCODING + " text"
417            + ");";
418        db.execSQL("create table " + Attachment.TABLE_NAME + s);
419    }
420
421    static void upgradeAttachmentTable(SQLiteDatabase db, int oldVersion, int newVersion) {
422        try {
423            db.execSQL("drop table " + Attachment.TABLE_NAME);
424        } catch (SQLException e) {
425        }
426        createAttachmentTable(db);
427    }
428
429    static void createBodyTable(SQLiteDatabase db) {
430        String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, "
431            + BodyColumns.MESSAGE_KEY + " integer, "
432            + BodyColumns.HTML_CONTENT + " text, "
433            + BodyColumns.TEXT_CONTENT + " text"
434            + ");";
435        db.execSQL("create table " + Body.TABLE_NAME + s);
436    }
437
438    static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) {
439        try {
440            db.execSQL("drop table " + Body.TABLE_NAME);
441        } catch (SQLException e) {
442        }
443        createBodyTable(db);
444    }
445
446    private final int mDatabaseVersion = DATABASE_VERSION;
447    private final int mBodyDatabaseVersion = BODY_DATABASE_VERSION;
448
449    private SQLiteDatabase mDatabase;
450    private SQLiteDatabase mBodyDatabase;
451
452    public SQLiteDatabase getDatabase(Context context) {
453        if (mDatabase !=  null) {
454            return mDatabase;
455        }
456        DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME);
457        mDatabase = helper.getWritableDatabase();
458        if (mDatabase != null) {
459            mDatabase.setLockingEnabled(true);
460        }
461        return mDatabase;
462    }
463
464    public SQLiteDatabase getBodyDatabase(Context context) {
465        if (mBodyDatabase !=  null) {
466            return mBodyDatabase;
467        }
468        BodyDatabaseHelper helper = new BodyDatabaseHelper(context, BODY_DATABASE_NAME);
469        mBodyDatabase = helper.getWritableDatabase();
470        if (mBodyDatabase != null) {
471            mBodyDatabase.setLockingEnabled(true);
472        }
473        return mBodyDatabase;
474    }
475
476    private class BodyDatabaseHelper extends SQLiteOpenHelper {
477        BodyDatabaseHelper(Context context, String name) {
478            super(context, name, null, mBodyDatabaseVersion);
479        }
480
481        @Override
482        public void onCreate(SQLiteDatabase db) {
483            // Create all tables here; each class has its own method
484            createBodyTable(db);
485        }
486
487        @Override
488        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
489            upgradeBodyTable(db, oldVersion, newVersion);
490        }
491
492        @Override
493        public void onOpen(SQLiteDatabase db) {
494        }
495    }
496
497    private class DatabaseHelper extends SQLiteOpenHelper {
498        DatabaseHelper(Context context, String name) {
499            super(context, name, null, mDatabaseVersion);
500        }
501
502        @Override
503        public void onCreate(SQLiteDatabase db) {
504            // Create all tables here; each class has its own method
505            createMessageTable(db);
506            createAttachmentTable(db);
507            createMailboxTable(db);
508            createHostAuthTable(db);
509            createAccountTable(db);
510        }
511
512        @Override
513        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
514            upgradeMessageTable(db, oldVersion, newVersion);
515            upgradeAttachmentTable(db, oldVersion, newVersion);
516            upgradeMailboxTable(db, oldVersion, newVersion);
517            upgradeHostAuthTable(db, oldVersion, newVersion);
518            upgradeAccountTable(db, oldVersion, newVersion);
519        }
520
521        @Override
522        public void onOpen(SQLiteDatabase db) {
523        }
524    }
525
526    @Override
527    public int delete(Uri uri, String selection, String[] selectionArgs) {
528        int match = sURIMatcher.match(uri);
529        Context context = getContext();
530        SQLiteDatabase db = (match >= BODY_BASE) ? getBodyDatabase(context) : getDatabase(context);
531        int table = match >> BASE_SHIFT;
532        String id = "0";
533        boolean attachBodyDb = false;
534        boolean deleteOrphanedBodies = false;
535
536        if (Email.LOGD) {
537            Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match);
538        }
539
540        int result = -1;
541
542        try {
543            switch (match) {
544                // These are cases in which one or more Messages might get deleted, either by
545                // cascade or explicitly
546                case MAILBOX_ID:
547                case MAILBOX:
548                case ACCOUNT_ID:
549                case ACCOUNT:
550                case MESSAGE:
551                case SYNCED_MESSAGE_ID:
552                case MESSAGE_ID:
553                    // Handle lost Body records here, since this cannot be done in a trigger
554                    // The process is:
555                    //  0) Activate the body database (bootstrap step, if doesn't exist yet)
556                    //  1) Attach the Body database
557                    //  2) Begin a transaction, ensuring that both databases are affected atomically
558                    //  3) Do the requested deletion, with cascading deletions handled in triggers
559                    //  4) End the transaction, committing all changes atomically
560                    //  5) Detach the Body database
561                    attachBodyDb = true;
562                    getBodyDatabase(context);
563                    String bodyFileName = mBodyDatabase.getPath();
564                    db.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
565                    db.beginTransaction();
566                    if (match != MESSAGE_ID) {
567                        deleteOrphanedBodies = true;
568                    }
569                    break;
570            }
571            switch (match) {
572                case BODY_ID:
573                case DELETED_MESSAGE_ID:
574                case SYNCED_MESSAGE_ID:
575                case MESSAGE_ID:
576                case UPDATED_MESSAGE_ID:
577                case ATTACHMENT_ID:
578                case MAILBOX_ID:
579                case ACCOUNT_ID:
580                case HOSTAUTH_ID:
581                    id = uri.getPathSegments().get(1);
582                    if (match == SYNCED_MESSAGE_ID) {
583                        // For synced messages, first copy the old message to the deleted table and
584                        // delete it from the updated table (in case it was updated first)
585                        // Note that this is all within a transaction, for atomicity
586                        db.execSQL(DELETED_MESSAGE_INSERT + id);
587                        db.execSQL(UPDATED_MESSAGE_DELETE + id);
588                    }
589                    result = db.delete(TABLE_NAMES[table], whereWithId(id, selection),
590                            selectionArgs);
591                    break;
592                case BODY:
593                case MESSAGE:
594                case DELETED_MESSAGE:
595                case UPDATED_MESSAGE:
596                case ATTACHMENT:
597                case MAILBOX:
598                case ACCOUNT:
599                case HOSTAUTH:
600                    result = db.delete(TABLE_NAMES[table], selection, selectionArgs);
601                    break;
602                default:
603                    throw new IllegalArgumentException("Unknown URI " + uri);
604            }
605            if (attachBodyDb) {
606                if (deleteOrphanedBodies) {
607                    // Delete any orphaned Body records
608                    db.execSQL(DELETE_ORPHAN_BODIES);
609                    db.setTransactionSuccessful();
610                } else {
611                    // Delete the Body record associated with the deleted message
612                    db.execSQL(DELETE_BODY + id);
613                    db.setTransactionSuccessful();
614                }
615            }
616        } finally {
617            if (attachBodyDb) {
618                db.endTransaction();
619                db.execSQL("detach BodyDatabase");
620            }
621        }
622        getContext().getContentResolver().notifyChange(uri, null);
623        return result;
624    }
625
626    @Override
627    // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
628    public String getType(Uri uri) {
629        int match = sURIMatcher.match(uri);
630        switch (match) {
631            case BODY_ID:
632                return "vnd.android.cursor.item/email-body";
633            case BODY:
634                return "vnd.android.cursor.dir/email-message";
635            case UPDATED_MESSAGE_ID:
636            case MESSAGE_ID:
637                return "vnd.android.cursor.item/email-message";
638            case MAILBOX_MESSAGES:
639            case UPDATED_MESSAGE:
640            case MESSAGE:
641                return "vnd.android.cursor.dir/email-message";
642            case ACCOUNT_MAILBOXES:
643            case MAILBOX:
644                return "vnd.android.cursor.dir/email-mailbox";
645            case MAILBOX_ID:
646                return "vnd.android.cursor.item/email-mailbox";
647            case ACCOUNT:
648                return "vnd.android.cursor.dir/email-account";
649            case ACCOUNT_ID:
650                return "vnd.android.cursor.item/email-account";
651            case ATTACHMENTS_MESSAGE_ID:
652            case ATTACHMENT:
653                return "vnd.android.cursor.dir/email-attachment";
654            case ATTACHMENT_ID:
655                return "vnd.android.cursor.item/email-attachment";
656            case HOSTAUTH:
657                return "vnd.android.cursor.dir/email-hostauth";
658            case HOSTAUTH_ID:
659                return "vnd.android.cursor.item/email-hostauth";
660            default:
661                throw new IllegalArgumentException("Unknown URI " + uri);
662        }
663    }
664
665    @Override
666    public Uri insert(Uri uri, ContentValues values) {
667        int match = sURIMatcher.match(uri);
668        Context context = getContext();
669        SQLiteDatabase db = (match >= BODY_BASE) ? getBodyDatabase(context) : getDatabase(context);
670        int table = match >> BASE_SHIFT;
671        long id;
672
673        if (Email.LOGD) {
674            Log.v(TAG, "EmailProvider.insert: uri=" + uri + ", match is " + match);
675        }
676
677        Uri resultUri = null;
678
679        switch (match) {
680            case BODY:
681            case MESSAGE:
682            case ATTACHMENT:
683            case MAILBOX:
684            case ACCOUNT:
685            case HOSTAUTH:
686                // Make sure all new message records have dirty count of 0
687                if (match == MESSAGE) {
688                    values.put(SyncColumns.DIRTY_COUNT, 0);
689                }
690                id = db.insert(TABLE_NAMES[table], "foo", values);
691                resultUri = ContentUris.withAppendedId(uri, id);
692                break;
693            case MAILBOX_ID:
694                // This implies adding a message to a mailbox
695                // Hmm, one problem here is that we can't link the account as well, so it must be
696                // already in the values...
697                id = Long.parseLong(uri.getPathSegments().get(1));
698                values.put(MessageColumns.MAILBOX_KEY, id);
699                resultUri = insert(Message.CONTENT_URI, values);
700                break;
701            case MESSAGE_ID:
702                // This implies adding an attachment to a message.
703                id = Long.parseLong(uri.getPathSegments().get(1));
704                values.put(AttachmentColumns.MESSAGE_KEY, id);
705                resultUri = insert(Attachment.CONTENT_URI, values);
706                break;
707            case ACCOUNT_ID:
708                // This implies adding a mailbox to an account.
709                id = Long.parseLong(uri.getPathSegments().get(1));
710                values.put(MailboxColumns.ACCOUNT_KEY, id);
711                resultUri = insert(Mailbox.CONTENT_URI, values);
712                break;
713            case ATTACHMENTS_MESSAGE_ID:
714                id = db.insert(TABLE_NAMES[table], "foo", values);
715                resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
716                break;
717            default:
718                throw new IllegalArgumentException("Unknown URL " + uri);
719        }
720
721        // Notify with the base uri, not the new uri (nobody is watching a new record)
722        getContext().getContentResolver().notifyChange(uri, null);
723        return resultUri;
724    }
725
726    @Override
727    public boolean onCreate() {
728        // TODO Auto-generated method stub
729        return false;
730    }
731
732    @Override
733    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
734            String sortOrder) {
735        Cursor c = null;
736        Uri notificationUri = EmailContent.CONTENT_URI;
737        int match = sURIMatcher.match(uri);
738        Context context = getContext();
739        SQLiteDatabase db = (match >= BODY_BASE) ? getBodyDatabase(context) : getDatabase(context);
740        int table = match >> BASE_SHIFT;
741        String id;
742
743        if (Email.LOGD) {
744            Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match);
745        }
746
747        switch (match) {
748            case BODY:
749            case MESSAGE:
750            case UPDATED_MESSAGE:
751            case DELETED_MESSAGE:
752            case ATTACHMENT:
753            case MAILBOX:
754            case ACCOUNT:
755            case HOSTAUTH:
756                c = db.query(TABLE_NAMES[table], projection,
757                        selection, selectionArgs, null, null, sortOrder);
758                break;
759            case BODY_ID:
760            case MESSAGE_ID:
761            case DELETED_MESSAGE_ID:
762            case UPDATED_MESSAGE_ID:
763            case ATTACHMENT_ID:
764            case MAILBOX_ID:
765            case ACCOUNT_ID:
766            case HOSTAUTH_ID:
767                id = uri.getPathSegments().get(1);
768                c = db.query(TABLE_NAMES[table], projection,
769                        whereWithId(id, selection), selectionArgs, null, null, sortOrder);
770                break;
771            case ATTACHMENTS_MESSAGE_ID:
772                // All attachments for the given message
773                id = uri.getPathSegments().get(2);
774                c = db.query(Attachment.TABLE_NAME, projection,
775                        whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
776                        selectionArgs, null, null, sortOrder);
777                break;
778            default:
779                throw new IllegalArgumentException("Unknown URI " + uri);
780        }
781
782        if ((c != null) && !isTemporary()) {
783            c.setNotificationUri(getContext().getContentResolver(), notificationUri);
784        }
785        return c;
786    }
787
788    private String whereWithId(String id, String selection) {
789        StringBuilder sb = new StringBuilder(256);
790        sb.append("_id=");
791        sb.append(id);
792        if (selection != null) {
793            sb.append(" AND ");
794            sb.append(selection);
795        }
796        return sb.toString();
797    }
798
799    private String whereWith(String where, String selection) {
800        StringBuilder sb = new StringBuilder(where);
801        if (selection != null) {
802            sb.append(" AND ");
803            sb.append(selection);
804        }
805        return sb.toString();
806    }
807
808    @Override
809    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
810        int match = sURIMatcher.match(uri);
811        Context context = getContext();
812        SQLiteDatabase db = (match >= BODY_BASE) ? getBodyDatabase(context) : getDatabase(context);
813        int table = match >> BASE_SHIFT;
814        int result;
815
816        if (Email.LOGD) {
817            Log.v(TAG, "EmailProvider.update: uri=" + uri + ", match is " + match);
818        }
819
820        switch (match) {
821            case BODY_ID:
822            case MESSAGE_ID:
823            case SYNCED_MESSAGE_ID:
824            case UPDATED_MESSAGE_ID:
825            case ATTACHMENT_ID:
826            case MAILBOX_ID:
827            case ACCOUNT_ID:
828            case HOSTAUTH_ID:
829                String id = uri.getPathSegments().get(1);
830                if (match == SYNCED_MESSAGE_ID) {
831                    // For synced messages, first copy the old message to the updated table
832                    // Note the insert or ignore semantics, guaranteeing that only the first
833                    // update will be reflected in the updated message table; therefore this row
834                    // will always have the "original" data
835                    db.execSQL(UPDATED_MESSAGE_INSERT + id);
836                } else if (match == MESSAGE_ID) {
837                    db.execSQL(UPDATED_MESSAGE_DELETE + id);
838                }
839                result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection),
840                        selectionArgs);
841                break;
842            case BODY:
843            case MESSAGE:
844            case UPDATED_MESSAGE:
845            case ATTACHMENT:
846            case MAILBOX:
847            case ACCOUNT:
848            case HOSTAUTH:
849                result = db.update(TABLE_NAMES[table], values, selection, selectionArgs);
850                break;
851            default:
852                throw new IllegalArgumentException("Unknown URI " + uri);
853        }
854
855        getContext().getContentResolver().notifyChange(uri, null);
856        return result;
857    }
858
859    /* (non-Javadoc)
860     * @see android.content.ContentProvider#applyBatch(android.content.ContentProviderOperation)
861     *
862     * TODO: How do we call notifyChange() or do we need to - does this call the various
863     * update/insert/delete calls?
864     */
865    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
866            throws OperationApplicationException {
867        SQLiteDatabase db = getDatabase(getContext());
868        db.beginTransaction();
869        try {
870            ContentProviderResult[] results = super.applyBatch(operations);
871            db.setTransactionSuccessful();
872            return results;
873        } finally {
874            db.endTransaction();
875        }
876    }
877}
878