EmailProvider.java revision 17d014dfdf172f3b60bcdb41f1b831fbdc8230d4
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.provider;
18
19import android.appwidget.AppWidgetManager;
20import android.content.ComponentName;
21import android.content.ContentProvider;
22import android.content.ContentProviderOperation;
23import android.content.ContentProviderResult;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.OperationApplicationException;
30import android.content.PeriodicSync;
31import android.content.UriMatcher;
32import android.content.res.Resources;
33import android.database.ContentObserver;
34import android.database.Cursor;
35import android.database.CursorWrapper;
36import android.database.DatabaseUtils;
37import android.database.MatrixCursor;
38import android.database.MergeCursor;
39import android.database.sqlite.SQLiteDatabase;
40import android.database.sqlite.SQLiteException;
41import android.net.Uri;
42import android.os.AsyncTask;
43import android.os.Binder;
44import android.os.Bundle;
45import android.os.Parcel;
46import android.os.ParcelFileDescriptor;
47import android.os.RemoteException;
48import android.provider.BaseColumns;
49import android.text.TextUtils;
50import android.text.format.DateUtils;
51import android.util.Base64;
52import android.util.SparseArray;
53
54import com.android.common.content.ProjectionMap;
55import com.android.email.Preferences;
56import com.android.email.R;
57import com.android.email.SecurityPolicy;
58import com.android.email.service.AttachmentDownloadService;
59import com.android.email.service.EmailServiceUtils;
60import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
61import com.android.email2.ui.MailActivityEmail;
62import com.android.emailcommon.Logging;
63import com.android.emailcommon.mail.Address;
64import com.android.emailcommon.provider.Account;
65import com.android.emailcommon.provider.EmailContent;
66import com.android.emailcommon.provider.EmailContent.AccountColumns;
67import com.android.emailcommon.provider.EmailContent.Attachment;
68import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
69import com.android.emailcommon.provider.EmailContent.Body;
70import com.android.emailcommon.provider.EmailContent.BodyColumns;
71import com.android.emailcommon.provider.EmailContent.MailboxColumns;
72import com.android.emailcommon.provider.EmailContent.Message;
73import com.android.emailcommon.provider.EmailContent.MessageColumns;
74import com.android.emailcommon.provider.EmailContent.PolicyColumns;
75import com.android.emailcommon.provider.EmailContent.SyncColumns;
76import com.android.emailcommon.provider.HostAuth;
77import com.android.emailcommon.provider.Mailbox;
78import com.android.emailcommon.provider.MessageChangeLogTable;
79import com.android.emailcommon.provider.MessageMove;
80import com.android.emailcommon.provider.MessageStateChange;
81import com.android.emailcommon.provider.Policy;
82import com.android.emailcommon.provider.QuickResponse;
83import com.android.emailcommon.service.EmailServiceProxy;
84import com.android.emailcommon.service.EmailServiceStatus;
85import com.android.emailcommon.service.IEmailService;
86import com.android.emailcommon.service.SearchParams;
87import com.android.emailcommon.utility.AttachmentUtilities;
88import com.android.emailcommon.utility.Utility;
89import com.android.ex.photo.provider.PhotoContract;
90import com.android.mail.preferences.MailPrefs;
91import com.android.mail.providers.Folder;
92import com.android.mail.providers.FolderList;
93import com.android.mail.providers.UIProvider;
94import com.android.mail.providers.UIProvider.AccountCapabilities;
95import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
96import com.android.mail.providers.UIProvider.ConversationPriority;
97import com.android.mail.providers.UIProvider.ConversationSendingState;
98import com.android.mail.providers.UIProvider.DraftType;
99import com.android.mail.utils.AttachmentUtils;
100import com.android.mail.utils.LogTag;
101import com.android.mail.utils.LogUtils;
102import com.android.mail.utils.MatrixCursorWithCachedColumns;
103import com.android.mail.utils.MatrixCursorWithExtra;
104import com.android.mail.utils.MimeType;
105import com.android.mail.utils.Utils;
106import com.android.mail.widget.BaseWidgetProvider;
107import com.android.mail.widget.WidgetProvider;
108import com.google.common.collect.ImmutableMap;
109import com.google.common.collect.ImmutableSet;
110import com.google.common.collect.Lists;
111
112import java.io.File;
113import java.io.FileDescriptor;
114import java.io.FileNotFoundException;
115import java.io.PrintWriter;
116import java.util.ArrayList;
117import java.util.Arrays;
118import java.util.List;
119import java.util.Locale;
120import java.util.Map;
121import java.util.Set;
122import java.util.regex.Pattern;
123
124/**
125 * @author mblank
126 *
127 */
128public class EmailProvider extends ContentProvider {
129
130    private static final String TAG = LogTag.getLogTag();
131
132    public static String EMAIL_APP_MIME_TYPE;
133
134    private static final String DATABASE_NAME = "EmailProvider.db";
135    private static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
136    private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db";
137
138    /**
139     * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this
140     * {@link android.content.Intent} and update accordingly. However, this can be very broad and
141     * is NOT the preferred way of getting notification.
142     */
143    private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED =
144        "com.android.email.MESSAGE_LIST_DATASET_CHANGED";
145
146    private static final String EMAIL_MESSAGE_MIME_TYPE =
147        "vnd.android.cursor.item/email-message";
148    private static final String EMAIL_ATTACHMENT_MIME_TYPE =
149        "vnd.android.cursor.item/email-attachment";
150
151    /** Appended to the notification URI for delete operations */
152    private static final String NOTIFICATION_OP_DELETE = "delete";
153    /** Appended to the notification URI for insert operations */
154    private static final String NOTIFICATION_OP_INSERT = "insert";
155    /** Appended to the notification URI for update operations */
156    private static final String NOTIFICATION_OP_UPDATE = "update";
157
158    /** The query string to trigger a folder refresh. */
159    private static String QUERY_UIREFRESH = "uirefresh";
160
161    // Definitions for our queries looking for orphaned messages
162    private static final String[] ORPHANS_PROJECTION
163        = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY};
164    private static final int ORPHANS_ID = 0;
165    private static final int ORPHANS_MAILBOX_KEY = 1;
166
167    private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
168
169    private static final int ACCOUNT_BASE = 0;
170    private static final int ACCOUNT = ACCOUNT_BASE;
171    private static final int ACCOUNT_ID = ACCOUNT_BASE + 1;
172    private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 2;
173    private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 3;
174    private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 4;
175    private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 5;
176    private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 6;
177
178    private static final int MAILBOX_BASE = 0x1000;
179    private static final int MAILBOX = MAILBOX_BASE;
180    private static final int MAILBOX_ID = MAILBOX_BASE + 1;
181    private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2;
182    private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3;
183    private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4;
184
185    private static final int MESSAGE_BASE = 0x2000;
186    private static final int MESSAGE = MESSAGE_BASE;
187    private static final int MESSAGE_ID = MESSAGE_BASE + 1;
188    private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
189    private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3;
190    private static final int MESSAGE_MOVE = MESSAGE_BASE + 4;
191    private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5;
192
193    private static final int ATTACHMENT_BASE = 0x3000;
194    private static final int ATTACHMENT = ATTACHMENT_BASE;
195    private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1;
196    private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2;
197    private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3;
198
199    private static final int HOSTAUTH_BASE = 0x4000;
200    private static final int HOSTAUTH = HOSTAUTH_BASE;
201    private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
202
203    private static final int UPDATED_MESSAGE_BASE = 0x5000;
204    private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
205    private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
206
207    private static final int DELETED_MESSAGE_BASE = 0x6000;
208    private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
209    private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
210
211    private static final int POLICY_BASE = 0x7000;
212    private static final int POLICY = POLICY_BASE;
213    private static final int POLICY_ID = POLICY_BASE + 1;
214
215    private static final int QUICK_RESPONSE_BASE = 0x8000;
216    private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE;
217    private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1;
218    private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2;
219
220    private static final int UI_BASE = 0x9000;
221    private static final int UI_FOLDERS = UI_BASE;
222    private static final int UI_SUBFOLDERS = UI_BASE + 1;
223    private static final int UI_MESSAGES = UI_BASE + 2;
224    private static final int UI_MESSAGE = UI_BASE + 3;
225    private static final int UI_UNDO = UI_BASE + 4;
226    private static final int UI_FOLDER_REFRESH = UI_BASE + 5;
227    private static final int UI_FOLDER = UI_BASE + 6;
228    private static final int UI_ACCOUNT = UI_BASE + 7;
229    private static final int UI_ACCTS = UI_BASE + 8;
230    private static final int UI_ATTACHMENTS = UI_BASE + 9;
231    private static final int UI_ATTACHMENT = UI_BASE + 10;
232    private static final int UI_SEARCH = UI_BASE + 11;
233    private static final int UI_ACCOUNT_DATA = UI_BASE + 12;
234    private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 13;
235    private static final int UI_CONVERSATION = UI_BASE + 14;
236    private static final int UI_RECENT_FOLDERS = UI_BASE + 15;
237    private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 16;
238    private static final int UI_ALL_FOLDERS = UI_BASE + 17;
239
240    private static final int BODY_BASE = 0xA000;
241    private static final int BODY = BODY_BASE;
242    private static final int BODY_ID = BODY_BASE + 1;
243
244    private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
245
246    private static final SparseArray<String> TABLE_NAMES;
247    static {
248        SparseArray<String> array = new SparseArray<String>(11);
249        array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME);
250        array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME);
251        array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME);
252        array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME);
253        array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME);
254        array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME);
255        array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME);
256        array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME);
257        array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME);
258        array.put(UI_BASE >> BASE_SHIFT, null);
259        array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME);
260        TABLE_NAMES = array;
261    }
262
263    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
264
265    /**
266     * Functions which manipulate the database connection or files synchronize on this.
267     * It's static because there can be multiple provider objects.
268     * TODO: Do we actually need to synchronize across all DB access, not just connection creation?
269     */
270    private static final Object sDatabaseLock = new Object();
271
272    /**
273     * Let's only generate these SQL strings once, as they are used frequently
274     * Note that this isn't relevant for table creation strings, since they are used only once
275     */
276    private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
277        Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
278        EmailContent.RECORD_ID + '=';
279
280    private static final String UPDATED_MESSAGE_DELETE = "delete from " +
281        Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '=';
282
283    private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
284        Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
285        EmailContent.RECORD_ID + '=';
286
287    private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
288        " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY +
289        " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " +
290        Message.TABLE_NAME + ')';
291
292    private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
293        " where " + BodyColumns.MESSAGE_KEY + '=';
294
295    private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
296    private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
297
298    private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId";
299
300    // For undo handling
301    private int mLastSequence = -1;
302    private final ArrayList<ContentProviderOperation> mLastSequenceOps =
303            new ArrayList<ContentProviderOperation>();
304
305    // Query parameter indicating the command came from UIProvider
306    private static final String IS_UIPROVIDER = "is_uiprovider";
307
308    private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status";
309
310    /**
311     * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
312     * @param uri the Uri to match
313     * @return the match value
314     */
315    private static int findMatch(Uri uri, String methodName) {
316        int match = sURIMatcher.match(uri);
317        if (match < 0) {
318            throw new IllegalArgumentException("Unknown uri: " + uri);
319        } else if (Logging.LOGD) {
320            LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
321        }
322        return match;
323    }
324
325    private static Uri INTEGRITY_CHECK_URI;
326    public static Uri ACCOUNT_BACKUP_URI;
327    private static Uri FOLDER_STATUS_URI;
328
329    private SQLiteDatabase mDatabase;
330    private SQLiteDatabase mBodyDatabase;
331
332    public static Uri uiUri(String type, long id) {
333        return Uri.parse(uiUriString(type, id));
334    }
335
336    /**
337     * Creates a URI string from a database ID (guaranteed to be unique).
338     * @param type of the resource: uifolder, message, etc.
339     * @param id the id of the resource.
340     * @return uri string
341     */
342    public static String uiUriString(String type, long id) {
343        return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id));
344    }
345
346    /**
347     * Orphan record deletion utility.  Generates a sqlite statement like:
348     *  delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>)
349     * @param db the EmailProvider database
350     * @param table the table whose orphans are to be removed
351     * @param column the column deletion will be based on
352     * @param foreignColumn the column in the foreign table whose absence will trigger the deletion
353     * @param foreignTable the foreign table
354     */
355    private static void deleteUnlinked(SQLiteDatabase db, String table, String column,
356            String foreignColumn, String foreignTable) {
357        int count = db.delete(table, column + " not in (select " + foreignColumn + " from " +
358                foreignTable + ")", null);
359        if (count > 0) {
360            LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table);
361        }
362    }
363
364    private SQLiteDatabase getDatabase(Context context) {
365        synchronized (sDatabaseLock) {
366            // Always return the cached database, if we've got one
367            if (mDatabase != null) {
368                return mDatabase;
369            }
370
371            // Whenever we create or re-cache the databases, make sure that we haven't lost one
372            // to corruption
373            checkDatabases();
374
375            DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
376            mDatabase = helper.getWritableDatabase();
377            DBHelper.BodyDatabaseHelper bodyHelper =
378                    new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME);
379            mBodyDatabase = bodyHelper.getWritableDatabase();
380            if (mBodyDatabase != null) {
381                String bodyFileName = mBodyDatabase.getPath();
382                mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
383            }
384
385            // Restore accounts if the database is corrupted...
386            restoreIfNeeded(context, mDatabase);
387            // Check for any orphaned Messages in the updated/deleted tables
388            deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
389            deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME);
390            // Delete orphaned mailboxes/messages/policies (account no longer exists)
391            deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY,
392                    AccountColumns.ID, Account.TABLE_NAME);
393            deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY,
394                    AccountColumns.ID, Account.TABLE_NAME);
395            deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID,
396                    AccountColumns.POLICY_KEY, Account.TABLE_NAME);
397            initUiProvider();
398            return mDatabase;
399        }
400    }
401
402    /**
403     * Perform startup actions related to UI
404     */
405    private void initUiProvider() {
406        // Clear mailbox sync status
407        mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS +
408                "=" + UIProvider.SyncStatus.NO_SYNC);
409    }
410
411    /**
412     * Restore user Account and HostAuth data from our backup database
413     */
414    private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
415        if (MailActivityEmail.DEBUG) {
416            LogUtils.w(TAG, "restoreIfNeeded...");
417        }
418        // Check for legacy backup
419        String legacyBackup = Preferences.getLegacyBackupPreference(context);
420        // If there's a legacy backup, create a new-style backup and delete the legacy backup
421        // In the 1:1000000000 chance that the user gets an app update just as his database becomes
422        // corrupt, oh well...
423        if (!TextUtils.isEmpty(legacyBackup)) {
424            backupAccounts(context, mainDatabase);
425            Preferences.clearLegacyBackupPreference(context);
426            LogUtils.w(TAG, "Created new EmailProvider backup database");
427            return;
428        }
429
430        // If we have accounts, we're done
431        if (DatabaseUtils.longForQuery(mainDatabase,
432                                      "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )",
433                                      EmailContent.ID_PROJECTION) > 0) {
434          if (MailActivityEmail.DEBUG) {
435              LogUtils.w(TAG, "restoreIfNeeded: Account exists.");
436          }
437          return;
438        }
439
440        restoreAccounts(context, mainDatabase);
441    }
442
443    /** {@inheritDoc} */
444    @Override
445    public void shutdown() {
446        if (mDatabase != null) {
447            mDatabase.close();
448            mDatabase = null;
449        }
450        if (mBodyDatabase != null) {
451            mBodyDatabase.close();
452            mBodyDatabase = null;
453        }
454    }
455
456    private static void deleteMessageOrphans(SQLiteDatabase database, String tableName) {
457        if (database != null) {
458            // We'll look at all of the items in the table; there won't be many typically
459            Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
460            // Usually, there will be nothing in these tables, so make a quick check
461            try {
462                if (c.getCount() == 0) return;
463                ArrayList<Long> foundMailboxes = new ArrayList<Long>();
464                ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
465                ArrayList<Long> deleteList = new ArrayList<Long>();
466                String[] bindArray = new String[1];
467                while (c.moveToNext()) {
468                    // Get the mailbox key and see if we've already found this mailbox
469                    // If so, we're fine
470                    long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
471                    // If we already know this mailbox doesn't exist, mark the message for deletion
472                    if (notFoundMailboxes.contains(mailboxId)) {
473                        deleteList.add(c.getLong(ORPHANS_ID));
474                    // If we don't know about this mailbox, we'll try to find it
475                    } else if (!foundMailboxes.contains(mailboxId)) {
476                        bindArray[0] = Long.toString(mailboxId);
477                        Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
478                                Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
479                        try {
480                            // If it exists, we'll add it to the "found" mailboxes
481                            if (boxCursor.moveToFirst()) {
482                                foundMailboxes.add(mailboxId);
483                            // Otherwise, we'll add to "not found" and mark the message for deletion
484                            } else {
485                                notFoundMailboxes.add(mailboxId);
486                                deleteList.add(c.getLong(ORPHANS_ID));
487                            }
488                        } finally {
489                            boxCursor.close();
490                        }
491                    }
492                }
493                // Now, delete the orphan messages
494                for (long messageId: deleteList) {
495                    bindArray[0] = Long.toString(messageId);
496                    database.delete(tableName, WHERE_ID, bindArray);
497                }
498            } finally {
499                c.close();
500            }
501        }
502    }
503
504    @Override
505    public int delete(Uri uri, String selection, String[] selectionArgs) {
506        final int match = findMatch(uri, "delete");
507        Context context = getContext();
508        // Pick the correct database for this operation
509        // If we're in a transaction already (which would happen during applyBatch), then the
510        // body database is already attached to the email database and any attempt to use the
511        // body database directly will result in a SQLiteException (the database is locked)
512        SQLiteDatabase db = getDatabase(context);
513        int table = match >> BASE_SHIFT;
514        String id = "0";
515        boolean messageDeletion = false;
516        ContentResolver resolver = context.getContentResolver();
517
518        String tableName = TABLE_NAMES.valueAt(table);
519        int result = -1;
520
521        try {
522            if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
523                if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
524                    notifyUIConversation(uri);
525                }
526            }
527            switch (match) {
528                case UI_MESSAGE:
529                    return uiDeleteMessage(uri);
530                case UI_ACCOUNT_DATA:
531                    return uiDeleteAccountData(uri);
532                case UI_ACCOUNT:
533                    return uiDeleteAccount(uri);
534                case MESSAGE_SELECTION:
535                    Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
536                            selectionArgs, null, null, null);
537                    try {
538                        if (findCursor.moveToFirst()) {
539                            return delete(ContentUris.withAppendedId(
540                                    Message.CONTENT_URI,
541                                    findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
542                                    null, null);
543                        } else {
544                            return 0;
545                        }
546                    } finally {
547                        findCursor.close();
548                    }
549                // These are cases in which one or more Messages might get deleted, either by
550                // cascade or explicitly
551                case MAILBOX_ID:
552                case MAILBOX:
553                case ACCOUNT_ID:
554                case ACCOUNT:
555                case MESSAGE:
556                case SYNCED_MESSAGE_ID:
557                case MESSAGE_ID:
558                    // Handle lost Body records here, since this cannot be done in a trigger
559                    // The process is:
560                    //  1) Begin a transaction, ensuring that both databases are affected atomically
561                    //  2) Do the requested deletion, with cascading deletions handled in triggers
562                    //  3) End the transaction, committing all changes atomically
563                    //
564                    // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
565                    messageDeletion = true;
566                    db.beginTransaction();
567                    break;
568            }
569            switch (match) {
570                case BODY_ID:
571                case DELETED_MESSAGE_ID:
572                case SYNCED_MESSAGE_ID:
573                case MESSAGE_ID:
574                case UPDATED_MESSAGE_ID:
575                case ATTACHMENT_ID:
576                case MAILBOX_ID:
577                case ACCOUNT_ID:
578                case HOSTAUTH_ID:
579                case POLICY_ID:
580                case QUICK_RESPONSE_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
590                    final long accountId;
591                    if (match == MAILBOX_ID) {
592                        accountId = Mailbox.getAccountIdForMailbox(context, id);
593                    } else {
594                        accountId = Account.NO_ACCOUNT;
595                    }
596
597                    result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
598
599                    if (match == ACCOUNT_ID) {
600                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
601                        resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
602                    } else if (match == MAILBOX_ID) {
603                        notifyUIFolder(id, accountId);
604                    } else if (match == ATTACHMENT_ID) {
605                        notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
606                    }
607                    break;
608                case ATTACHMENTS_MESSAGE_ID:
609                    // All attachments for the given message
610                    id = uri.getPathSegments().get(2);
611                    result = db.delete(tableName,
612                            whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
613                    break;
614
615                case BODY:
616                case MESSAGE:
617                case DELETED_MESSAGE:
618                case UPDATED_MESSAGE:
619                case ATTACHMENT:
620                case MAILBOX:
621                case ACCOUNT:
622                case HOSTAUTH:
623                case POLICY:
624                    result = db.delete(tableName, selection, selectionArgs);
625                    break;
626                case MESSAGE_MOVE:
627                    db.delete(MessageMove.TABLE_NAME, selection, selectionArgs);
628                    break;
629                case MESSAGE_STATE_CHANGE:
630                    db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs);
631                    break;
632                default:
633                    throw new IllegalArgumentException("Unknown URI " + uri);
634            }
635            if (messageDeletion) {
636                if (match == MESSAGE_ID) {
637                    // Delete the Body record associated with the deleted message
638                    db.execSQL(DELETE_BODY + id);
639                } else {
640                    // Delete any orphaned Body records
641                    db.execSQL(DELETE_ORPHAN_BODIES);
642                }
643                db.setTransactionSuccessful();
644            }
645        } catch (SQLiteException e) {
646            checkDatabases();
647            throw e;
648        } finally {
649            if (messageDeletion) {
650                db.endTransaction();
651            }
652        }
653
654        // Notify all notifier cursors
655        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
656
657        // Notify all email content cursors
658        resolver.notifyChange(EmailContent.CONTENT_URI, null);
659        return result;
660    }
661
662    @Override
663    // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
664    public String getType(Uri uri) {
665        int match = findMatch(uri, "getType");
666        switch (match) {
667            case BODY_ID:
668                return "vnd.android.cursor.item/email-body";
669            case BODY:
670                return "vnd.android.cursor.dir/email-body";
671            case UPDATED_MESSAGE_ID:
672            case MESSAGE_ID:
673                // NOTE: According to the framework folks, we're supposed to invent mime types as
674                // a way of passing information to drag & drop recipients.
675                // If there's a mailboxId parameter in the url, we respond with a mime type that
676                // has -n appended, where n is the mailboxId of the message.  The drag & drop code
677                // uses this information to know not to allow dragging the item to its own mailbox
678                String mimeType = EMAIL_MESSAGE_MIME_TYPE;
679                String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID);
680                if (mailboxId != null) {
681                    mimeType += "-" + mailboxId;
682                }
683                return mimeType;
684            case UPDATED_MESSAGE:
685            case MESSAGE:
686                return "vnd.android.cursor.dir/email-message";
687            case MAILBOX:
688                return "vnd.android.cursor.dir/email-mailbox";
689            case MAILBOX_ID:
690                return "vnd.android.cursor.item/email-mailbox";
691            case ACCOUNT:
692                return "vnd.android.cursor.dir/email-account";
693            case ACCOUNT_ID:
694                return "vnd.android.cursor.item/email-account";
695            case ATTACHMENTS_MESSAGE_ID:
696            case ATTACHMENT:
697                return "vnd.android.cursor.dir/email-attachment";
698            case ATTACHMENT_ID:
699                return EMAIL_ATTACHMENT_MIME_TYPE;
700            case HOSTAUTH:
701                return "vnd.android.cursor.dir/email-hostauth";
702            case HOSTAUTH_ID:
703                return "vnd.android.cursor.item/email-hostauth";
704            default:
705                return null;
706        }
707    }
708
709    // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI
710    // as the base because that gets spammed.
711    // These can't be statically initialized because they depend on EmailContent.AUTHORITY
712    private static Uri UIPROVIDER_CONVERSATION_NOTIFIER;
713    private static Uri UIPROVIDER_FOLDER_NOTIFIER;
714    private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER;
715    private static Uri UIPROVIDER_ACCOUNT_NOTIFIER;
716    // Not currently used
717    //public static Uri UIPROVIDER_SETTINGS_NOTIFIER;
718    private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER;
719    private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER;
720    public static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER;
721    private static Uri UIPROVIDER_MESSAGE_NOTIFIER;
722    private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER;
723
724    @Override
725    public Uri insert(Uri uri, ContentValues values) {
726        int match = findMatch(uri, "insert");
727        Context context = getContext();
728        ContentResolver resolver = context.getContentResolver();
729
730        // See the comment at delete(), above
731        SQLiteDatabase db = getDatabase(context);
732        int table = match >> BASE_SHIFT;
733        String id = "0";
734        long longId;
735
736        // We do NOT allow setting of unreadCount/messageCount via the provider
737        // These columns are maintained via triggers
738        if (match == MAILBOX_ID || match == MAILBOX) {
739            values.put(MailboxColumns.UNREAD_COUNT, 0);
740            values.put(MailboxColumns.MESSAGE_COUNT, 0);
741        }
742
743        final Uri resultUri;
744
745        try {
746            switch (match) {
747                // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
748                // or DELETED_MESSAGE; see the comment below for details
749                case UPDATED_MESSAGE:
750                case DELETED_MESSAGE:
751                case MESSAGE:
752                case BODY:
753                case ATTACHMENT:
754                case MAILBOX:
755                case ACCOUNT:
756                case HOSTAUTH:
757                case POLICY:
758                case QUICK_RESPONSE:
759                    longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
760                    resultUri = ContentUris.withAppendedId(uri, longId);
761                    switch(match) {
762                        case MESSAGE:
763                            final long mailboxId = values.getAsLong(Message.MAILBOX_KEY);
764                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
765                                notifyUIConversationMailbox(mailboxId);
766                            }
767                            notifyUIFolder(mailboxId, values.getAsLong(Message.ACCOUNT_KEY));
768                            break;
769                        case MAILBOX:
770                            if (values.containsKey(MailboxColumns.TYPE)) {
771                                if (values.getAsInteger(MailboxColumns.TYPE) <
772                                        Mailbox.TYPE_NOT_EMAIL) {
773                                    // Notify the account when a new mailbox is added
774                                    final Long accountId =
775                                            values.getAsLong(MailboxColumns.ACCOUNT_KEY);
776                                    if (accountId != null && accountId > 0) {
777                                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId);
778                                        notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
779                                    }
780                                }
781                            }
782                            break;
783                        case ACCOUNT:
784                            updateAccountSyncInterval(longId, values);
785                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
786                                notifyUIAccount(longId);
787                            }
788                            resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
789                            break;
790                        case UPDATED_MESSAGE:
791                        case DELETED_MESSAGE:
792                            throw new IllegalArgumentException("Unknown URL " + uri);
793                        case ATTACHMENT:
794                            int flags = 0;
795                            if (values.containsKey(Attachment.FLAGS)) {
796                                flags = values.getAsInteger(Attachment.FLAGS);
797                            }
798                            // Report all new attachments to the download service
799                            mAttachmentService.attachmentChanged(getContext(), longId, flags);
800                            break;
801                    }
802                    break;
803                case QUICK_RESPONSE_ACCOUNT_ID:
804                    longId = Long.parseLong(uri.getPathSegments().get(2));
805                    values.put(EmailContent.QuickResponseColumns.ACCOUNT_KEY, longId);
806                    return insert(QuickResponse.CONTENT_URI, values);
807                case MAILBOX_ID:
808                    // This implies adding a message to a mailbox
809                    // Hmm, a problem here is that we can't link the account as well, so it must be
810                    // already in the values...
811                    longId = Long.parseLong(uri.getPathSegments().get(1));
812                    values.put(MessageColumns.MAILBOX_KEY, longId);
813                    return insert(Message.CONTENT_URI, values); // Recurse
814                case MESSAGE_ID:
815                    // This implies adding an attachment to a message.
816                    id = uri.getPathSegments().get(1);
817                    longId = Long.parseLong(id);
818                    values.put(AttachmentColumns.MESSAGE_KEY, longId);
819                    return insert(Attachment.CONTENT_URI, values); // Recurse
820                case ACCOUNT_ID:
821                    // This implies adding a mailbox to an account.
822                    longId = Long.parseLong(uri.getPathSegments().get(1));
823                    values.put(MailboxColumns.ACCOUNT_KEY, longId);
824                    return insert(Mailbox.CONTENT_URI, values); // Recurse
825                case ATTACHMENTS_MESSAGE_ID:
826                    longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
827                    resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
828                    break;
829                default:
830                    throw new IllegalArgumentException("Unknown URL " + uri);
831            }
832        } catch (SQLiteException e) {
833            checkDatabases();
834            throw e;
835        }
836
837        // Notify all notifier cursors
838        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
839
840        // Notify all existing cursors.
841        resolver.notifyChange(EmailContent.CONTENT_URI, null);
842        return resultUri;
843    }
844
845    @Override
846    public boolean onCreate() {
847        Context context = getContext();
848        EmailContent.init(context);
849        init(context);
850        // Do this last, so that EmailContent/EmailProvider are initialized
851        MailActivityEmail.setServicesEnabledAsync(context);
852        return false;
853    }
854
855    private static void init(final Context context) {
856        // Synchronize on the matcher rather than the class object to minimize risk of contention
857        // & deadlock.
858        synchronized (sURIMatcher) {
859            // We use the existence of this variable as indicative of whether this function has
860            // already run.
861            if (INTEGRITY_CHECK_URI != null) {
862                return;
863            }
864            INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY +
865                    "/integrityCheck");
866            ACCOUNT_BACKUP_URI =
867                    Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup");
868            FOLDER_STATUS_URI =
869                    Uri.parse("content://" + EmailContent.AUTHORITY + "/status");
870            EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type);
871
872            final String uiNotificationAuthority =
873                    EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications";
874            UIPROVIDER_CONVERSATION_NOTIFIER =
875                    Uri.parse("content://" + uiNotificationAuthority + "/uimessages");
876            UIPROVIDER_FOLDER_NOTIFIER =
877                    Uri.parse("content://" + uiNotificationAuthority + "/uifolder");
878            UIPROVIDER_FOLDERLIST_NOTIFIER =
879                    Uri.parse("content://" + uiNotificationAuthority + "/uifolders");
880            UIPROVIDER_ACCOUNT_NOTIFIER =
881                    Uri.parse("content://" + uiNotificationAuthority + "/uiaccount");
882            // Not currently used
883            /* UIPROVIDER_SETTINGS_NOTIFIER =
884                    Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/
885            UIPROVIDER_ATTACHMENT_NOTIFIER =
886                    Uri.parse("content://" + uiNotificationAuthority + "/uiattachment");
887            UIPROVIDER_ATTACHMENTS_NOTIFIER =
888                    Uri.parse("content://" + uiNotificationAuthority + "/uiattachments");
889            UIPROVIDER_ALL_ACCOUNTS_NOTIFIER =
890                    Uri.parse("content://" + uiNotificationAuthority + "/uiaccts");
891            UIPROVIDER_MESSAGE_NOTIFIER =
892                    Uri.parse("content://" + uiNotificationAuthority + "/uimessage");
893            UIPROVIDER_RECENT_FOLDERS_NOTIFIER =
894                    Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders");
895
896
897            // All accounts
898            sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT);
899            // A specific account
900            // insert into this URI causes a mailbox to be added to the account
901            sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID);
902            sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK);
903
904            // Special URI to reset the new message count.  Only update works, and values
905            // will be ignored.
906            sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount",
907                    ACCOUNT_RESET_NEW_COUNT);
908            sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#",
909                    ACCOUNT_RESET_NEW_COUNT_ID);
910
911            // All mailboxes
912            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX);
913            // A specific mailbox
914            // insert into this URI causes a message to be added to the mailbox
915            // ** NOTE For now, the accountKey must be set manually in the values!
916            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID);
917            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#",
918                    MAILBOX_NOTIFICATION);
919            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#",
920                    MAILBOX_MOST_RECENT_MESSAGE);
921            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT);
922
923            // All messages
924            sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE);
925            // A specific message
926            // insert into this URI causes an attachment to be added to the message
927            sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID);
928
929            // A specific attachment
930            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT);
931            // A specific attachment (the header information)
932            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID);
933            // The attachments of a specific message (query only) (insert & delete TBD)
934            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#",
935                    ATTACHMENTS_MESSAGE_ID);
936            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile",
937                    ATTACHMENTS_CACHED_FILE_ACCESS);
938
939            // All mail bodies
940            sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
941            // A specific mail body
942            sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
943
944            // All hostauth records
945            sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
946            // A specific hostauth
947            sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID);
948
949            /**
950             * THIS URI HAS SPECIAL SEMANTICS
951             * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK
952             * TO A SERVER VIA A SYNC ADAPTER
953             */
954            sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
955            sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION);
956
957            sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE);
958            sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH,
959                    MESSAGE_STATE_CHANGE);
960
961            /**
962             * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
963             * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
964             * BY THE UI APPLICATION
965             */
966            // All deleted messages
967            sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE);
968            // A specific deleted message
969            sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
970
971            // All updated messages
972            sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
973            // A specific updated message
974            sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
975
976            CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues();
977            CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0);
978
979            sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY);
980            sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID);
981
982            // All quick responses
983            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE);
984            // A specific quick response
985            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID);
986            // All quick responses associated with a particular account id
987            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#",
988                    QUICK_RESPONSE_ACCOUNT_ID);
989
990            sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS);
991            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS);
992            sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS);
993            sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES);
994            sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE);
995            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO);
996            sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH);
997            // We listen to everything trailing uifolder/ since there might be an appVersion
998            // as in Utils.appendVersionQueryParameter().
999            sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER);
1000            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT);
1001            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS);
1002            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS);
1003            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT);
1004            sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
1005            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
1006            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
1007            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
1008            sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS);
1009            sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
1010                    UI_DEFAULT_RECENT_FOLDERS);
1011            sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#",
1012                    ACCOUNT_PICK_TRASH_FOLDER);
1013            sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#",
1014                    ACCOUNT_PICK_SENT_FOLDER);
1015        }
1016    }
1017
1018    /**
1019     * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
1020     * always be in sync (i.e. there are two database or NO databases).  This code will delete
1021     * any "orphan" database, so that both will be created together.  Note that an "orphan" database
1022     * will exist after either of the individual databases is deleted due to data corruption.
1023     */
1024    public void checkDatabases() {
1025        synchronized (sDatabaseLock) {
1026            // Uncache the databases
1027            if (mDatabase != null) {
1028                mDatabase = null;
1029            }
1030            if (mBodyDatabase != null) {
1031                mBodyDatabase = null;
1032            }
1033            // Look for orphans, and delete as necessary; these must always be in sync
1034            final File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
1035            final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
1036
1037            // TODO Make sure attachments are deleted
1038            if (databaseFile.exists() && !bodyFile.exists()) {
1039                LogUtils.w(TAG, "Deleting orphaned EmailProvider database...");
1040                getContext().deleteDatabase(DATABASE_NAME);
1041            } else if (bodyFile.exists() && !databaseFile.exists()) {
1042                LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database...");
1043                getContext().deleteDatabase(BODY_DATABASE_NAME);
1044            }
1045        }
1046    }
1047
1048    @Override
1049    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1050            String sortOrder) {
1051        Cursor c = null;
1052        int match;
1053        try {
1054            match = findMatch(uri, "query");
1055        } catch (IllegalArgumentException e) {
1056            String uriString = uri.toString();
1057            // If we were passed an illegal uri, see if it ends in /-1
1058            // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
1059            if (uriString != null && uriString.endsWith("/-1")) {
1060                uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
1061                match = findMatch(uri, "query");
1062                switch (match) {
1063                    case BODY_ID:
1064                    case MESSAGE_ID:
1065                    case DELETED_MESSAGE_ID:
1066                    case UPDATED_MESSAGE_ID:
1067                    case ATTACHMENT_ID:
1068                    case MAILBOX_ID:
1069                    case ACCOUNT_ID:
1070                    case HOSTAUTH_ID:
1071                    case POLICY_ID:
1072                        return new MatrixCursorWithCachedColumns(projection, 0);
1073                }
1074            }
1075            throw e;
1076        }
1077        Context context = getContext();
1078        // See the comment at delete(), above
1079        SQLiteDatabase db = getDatabase(context);
1080        int table = match >> BASE_SHIFT;
1081        String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
1082        String id;
1083
1084        String tableName = TABLE_NAMES.valueAt(table);
1085
1086        try {
1087            switch (match) {
1088                // First, dispatch queries from UnfiedEmail
1089                case UI_SEARCH:
1090                    c = uiSearch(uri, projection);
1091                    return c;
1092                case UI_ACCTS:
1093                    c = uiAccounts(projection);
1094                    return c;
1095                case UI_UNDO:
1096                    return uiUndo(projection);
1097                case UI_SUBFOLDERS:
1098                case UI_MESSAGES:
1099                case UI_MESSAGE:
1100                case UI_FOLDER:
1101                case UI_ACCOUNT:
1102                case UI_ATTACHMENT:
1103                case UI_ATTACHMENTS:
1104                case UI_CONVERSATION:
1105                case UI_RECENT_FOLDERS:
1106                case UI_ALL_FOLDERS:
1107                    // For now, we don't allow selection criteria within these queries
1108                    if (selection != null || selectionArgs != null) {
1109                        throw new IllegalArgumentException("UI queries can't have selection/args");
1110                    }
1111
1112                    final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER);
1113                    final boolean unseenOnly =
1114                            seenParam != null && Boolean.FALSE.toString().equals(seenParam);
1115
1116                    c = uiQuery(match, uri, projection, unseenOnly);
1117                    return c;
1118                case UI_FOLDERS:
1119                    c = uiFolders(uri, projection);
1120                    return c;
1121                case UI_FOLDER_LOAD_MORE:
1122                    c = uiFolderLoadMore(getMailbox(uri));
1123                    return c;
1124                case UI_FOLDER_REFRESH:
1125                    c = uiFolderRefresh(getMailbox(uri), 0);
1126                    return c;
1127                case MAILBOX_NOTIFICATION:
1128                    c = notificationQuery(uri);
1129                    return c;
1130                case MAILBOX_MOST_RECENT_MESSAGE:
1131                    c = mostRecentMessageQuery(uri);
1132                    return c;
1133                case MAILBOX_MESSAGE_COUNT:
1134                    c = getMailboxMessageCount(uri);
1135                    return c;
1136                case MESSAGE_MOVE:
1137                    return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs,
1138                            null, null, sortOrder, limit);
1139                case MESSAGE_STATE_CHANGE:
1140                    return db.query(MessageStateChange.TABLE_NAME, projection, selection,
1141                            selectionArgs, null, null, sortOrder, limit);
1142                case BODY:
1143                case MESSAGE:
1144                case UPDATED_MESSAGE:
1145                case DELETED_MESSAGE:
1146                case ATTACHMENT:
1147                case MAILBOX:
1148                case ACCOUNT:
1149                case HOSTAUTH:
1150                case POLICY:
1151                    c = db.query(tableName, projection,
1152                            selection, selectionArgs, null, null, sortOrder, limit);
1153                    break;
1154                case QUICK_RESPONSE:
1155                    c = uiQuickResponse(projection);
1156                    break;
1157                case BODY_ID:
1158                case MESSAGE_ID:
1159                case DELETED_MESSAGE_ID:
1160                case UPDATED_MESSAGE_ID:
1161                case ATTACHMENT_ID:
1162                case MAILBOX_ID:
1163                case ACCOUNT_ID:
1164                case HOSTAUTH_ID:
1165                case POLICY_ID:
1166                    id = uri.getPathSegments().get(1);
1167                    c = db.query(tableName, projection, whereWithId(id, selection),
1168                            selectionArgs, null, null, sortOrder, limit);
1169                    break;
1170                case QUICK_RESPONSE_ID:
1171                    id = uri.getPathSegments().get(1);
1172                    c = uiQuickResponseId(projection, id);
1173                    break;
1174                case ATTACHMENTS_MESSAGE_ID:
1175                    // All attachments for the given message
1176                    id = uri.getPathSegments().get(2);
1177                    c = db.query(Attachment.TABLE_NAME, projection,
1178                            whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
1179                            selectionArgs, null, null, sortOrder, limit);
1180                    break;
1181                case QUICK_RESPONSE_ACCOUNT_ID:
1182                    // All quick responses for the given account
1183                    id = uri.getPathSegments().get(2);
1184                    c = uiQuickResponseAccount(projection, id);
1185                    break;
1186                default:
1187                    throw new IllegalArgumentException("Unknown URI " + uri);
1188            }
1189        } catch (SQLiteException e) {
1190            checkDatabases();
1191            throw e;
1192        } catch (RuntimeException e) {
1193            checkDatabases();
1194            e.printStackTrace();
1195            throw e;
1196        } finally {
1197            if (c == null) {
1198                // This should never happen, but let's be sure to log it...
1199                // TODO: There are actually cases where c == null is expected, for example
1200                // UI_FOLDER_LOAD_MORE.
1201                // Demoting this to a warning for now until we figure out what to do with it.
1202                LogUtils.w(TAG, "Query returning null for uri: " + uri + ", selection: "
1203                        + selection);
1204            }
1205        }
1206
1207        if ((c != null) && !isTemporary()) {
1208            c.setNotificationUri(getContext().getContentResolver(), uri);
1209        }
1210        return c;
1211    }
1212
1213    private static String whereWithId(String id, String selection) {
1214        StringBuilder sb = new StringBuilder(256);
1215        sb.append("_id=");
1216        sb.append(id);
1217        if (selection != null) {
1218            sb.append(" AND (");
1219            sb.append(selection);
1220            sb.append(')');
1221        }
1222        return sb.toString();
1223    }
1224
1225    /**
1226     * Combine a locally-generated selection with a user-provided selection
1227     *
1228     * This introduces risk that the local selection might insert incorrect chars
1229     * into the SQL, so use caution.
1230     *
1231     * @param where locally-generated selection, must not be null
1232     * @param selection user-provided selection, may be null
1233     * @return a single selection string
1234     */
1235    private static String whereWith(String where, String selection) {
1236        if (selection == null) {
1237            return where;
1238        }
1239        StringBuilder sb = new StringBuilder(where);
1240        sb.append(" AND (");
1241        sb.append(selection);
1242        sb.append(')');
1243
1244        return sb.toString();
1245    }
1246
1247    /**
1248     * Restore a HostAuth from a database, given its unique id
1249     * @param db the database
1250     * @param id the unique id (_id) of the row
1251     * @return a fully populated HostAuth or null if the row does not exist
1252     */
1253    private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
1254        Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
1255                HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null);
1256        try {
1257            if (c.moveToFirst()) {
1258                HostAuth hostAuth = new HostAuth();
1259                hostAuth.restore(c);
1260                return hostAuth;
1261            }
1262            return null;
1263        } finally {
1264            c.close();
1265        }
1266    }
1267
1268    /**
1269     * Copy the Account and HostAuth tables from one database to another
1270     * @param fromDatabase the source database
1271     * @param toDatabase the destination database
1272     * @return the number of accounts copied, or -1 if an error occurred
1273     */
1274    private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
1275        if (fromDatabase == null || toDatabase == null) return -1;
1276
1277        // Lock both databases; for the "from" database, we don't want anyone changing it from
1278        // under us; for the "to" database, we want to make the operation atomic
1279        int copyCount = 0;
1280        fromDatabase.beginTransaction();
1281        try {
1282            toDatabase.beginTransaction();
1283            try {
1284                // Delete anything hanging around here
1285                toDatabase.delete(Account.TABLE_NAME, null, null);
1286                toDatabase.delete(HostAuth.TABLE_NAME, null, null);
1287
1288                // Get our account cursor
1289                Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
1290                        null, null, null, null, null);
1291                if (c == null) return 0;
1292                LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount());
1293                try {
1294                    // Loop through accounts, copying them and associated host auth's
1295                    while (c.moveToNext()) {
1296                        Account account = new Account();
1297                        account.restore(c);
1298
1299                        // Clear security sync key and sync key, as these were specific to the
1300                        // state of the account, and we've reset that...
1301                        // Clear policy key so that we can re-establish policies from the server
1302                        // TODO This is pretty EAS specific, but there's a lot of that around
1303                        account.mSecuritySyncKey = null;
1304                        account.mSyncKey = null;
1305                        account.mPolicyKey = 0;
1306
1307                        // Copy host auth's and update foreign keys
1308                        HostAuth hostAuth = restoreHostAuth(fromDatabase,
1309                                account.mHostAuthKeyRecv);
1310
1311                        // The account might have gone away, though very unlikely
1312                        if (hostAuth == null) continue;
1313                        account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
1314                                hostAuth.toContentValues());
1315
1316                        // EAS accounts have no send HostAuth
1317                        if (account.mHostAuthKeySend > 0) {
1318                            hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
1319                            // Belt and suspenders; I can't imagine that this is possible,
1320                            // since we checked the validity of the account above, and the
1321                            // database is now locked
1322                            if (hostAuth == null) continue;
1323                            account.mHostAuthKeySend = toDatabase.insert(
1324                                    HostAuth.TABLE_NAME, null, hostAuth.toContentValues());
1325                        }
1326
1327                        // Now, create the account in the "to" database
1328                        toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
1329                        copyCount++;
1330                    }
1331                } finally {
1332                    c.close();
1333                }
1334
1335                // Say it's ok to commit
1336                toDatabase.setTransactionSuccessful();
1337            } finally {
1338                toDatabase.endTransaction();
1339            }
1340        } catch (SQLiteException ex) {
1341            LogUtils.w(TAG, "Exception while copying account tables", ex);
1342            copyCount = -1;
1343        } finally {
1344            fromDatabase.endTransaction();
1345        }
1346        return copyCount;
1347    }
1348
1349    private static SQLiteDatabase getBackupDatabase(Context context) {
1350        DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME);
1351        return helper.getWritableDatabase();
1352    }
1353
1354    /**
1355     * Backup account data, returning the number of accounts backed up
1356     */
1357    private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) {
1358        if (MailActivityEmail.DEBUG) {
1359            LogUtils.d(TAG, "backupAccounts...");
1360        }
1361        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1362        try {
1363            int numBackedUp = copyAccountTables(mainDatabase, backupDatabase);
1364            if (numBackedUp < 0) {
1365                LogUtils.e(TAG, "Account backup failed!");
1366            } else if (MailActivityEmail.DEBUG) {
1367                LogUtils.d(TAG, "Backed up " + numBackedUp + " accounts...");
1368            }
1369            return numBackedUp;
1370        } finally {
1371            if (backupDatabase != null) {
1372                backupDatabase.close();
1373            }
1374        }
1375    }
1376
1377    /**
1378     * Restore account data, returning the number of accounts restored
1379     */
1380    private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) {
1381        if (MailActivityEmail.DEBUG) {
1382            LogUtils.d(TAG, "restoreAccounts...");
1383        }
1384        SQLiteDatabase backupDatabase = getBackupDatabase(context);
1385        try {
1386            int numRecovered = copyAccountTables(backupDatabase, mainDatabase);
1387            if (numRecovered > 0) {
1388                LogUtils.e(TAG, "Recovered " + numRecovered + " accounts!");
1389            } else if (numRecovered < 0) {
1390                LogUtils.e(TAG, "Account recovery failed?");
1391            } else if (MailActivityEmail.DEBUG) {
1392                LogUtils.d(TAG, "No accounts to restore...");
1393            }
1394            return numRecovered;
1395        } finally {
1396            if (backupDatabase != null) {
1397                backupDatabase.close();
1398            }
1399        }
1400    }
1401
1402    private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s ("
1403            + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + ","
1404            + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ",";
1405
1406    private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, "
1407            + "(select " + Message.SERVER_ID + " from " + Message.TABLE_NAME + " where _id=%s),"
1408            + "(select " + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
1409            + MessageMove.STATUS_NONE_STRING + ",";
1410
1411    /**
1412     * Formatting string to generate the SQL statement for inserting into MessageMove.
1413     * The formatting parameters are:
1414     * table name, message id x 4, destination folder id, message id, destination folder id.
1415     * Duplications are needed for sub-selects.
1416     */
1417    private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
1418            + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + ","
1419            + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID
1420            + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
1421            + "(select " + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s),"
1422            + "%d,"
1423            + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select "
1424            + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)),"
1425            + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))";
1426
1427    /**
1428     * Insert a row into the MessageMove table when that message is moved.
1429     * @param db The {@link SQLiteDatabase}.
1430     * @param messageId The id of the message being moved.
1431     * @param dstFolderKey The folder to which the message is being moved.
1432     */
1433    private void addToMessageMove(final SQLiteDatabase db, final String messageId,
1434            final long dstFolderKey) {
1435        db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME,
1436                messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey));
1437    }
1438
1439    /**
1440     * Formatting string to generate the SQL statement for inserting into MessageStateChange.
1441     * The formatting parameters are:
1442     * table name, message id x 4, new flag read, message id, new flag favorite.
1443     * Duplications are needed for sub-selects.
1444     */
1445    private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
1446            + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + ","
1447            + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE
1448            + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
1449            + "(select " + Message.FLAG_READ + " from " + Message.TABLE_NAME + " where _id=%s),"
1450            + "%d,"
1451            + "(select " + Message.FLAG_FAVORITE + " from " + Message.TABLE_NAME + " where _id=%s),"
1452            + "%d)";
1453
1454    private void addToMessageStateChange(final SQLiteDatabase db, final String messageId,
1455            final int newFlagRead, final int newFlagFavorite) {
1456        db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT,
1457                MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId,
1458                newFlagRead, messageId, newFlagFavorite));
1459    }
1460
1461    // select count(*) from (select count(*) as dupes from Mailbox where accountKey=?
1462    // group by serverId) where dupes > 1;
1463    private static final String ACCOUNT_INTEGRITY_SQL =
1464            "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME +
1465            " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1";
1466
1467    @Override
1468    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1469        // Handle this special case the fastest possible way
1470        if (uri == INTEGRITY_CHECK_URI) {
1471            checkDatabases();
1472            return 0;
1473        } else if (uri == ACCOUNT_BACKUP_URI) {
1474            return backupAccounts(getContext(), getDatabase(getContext()));
1475        }
1476
1477        // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
1478        Uri notificationUri = EmailContent.CONTENT_URI;
1479
1480        int match = findMatch(uri, "update");
1481        Context context = getContext();
1482        ContentResolver resolver = context.getContentResolver();
1483        // See the comment at delete(), above
1484        SQLiteDatabase db = getDatabase(context);
1485        int table = match >> BASE_SHIFT;
1486        int result;
1487
1488        // We do NOT allow setting of unreadCount/messageCount via the provider
1489        // These columns are maintained via triggers
1490        if (match == MAILBOX_ID || match == MAILBOX) {
1491            values.remove(MailboxColumns.UNREAD_COUNT);
1492            values.remove(MailboxColumns.MESSAGE_COUNT);
1493        }
1494
1495        String tableName = TABLE_NAMES.valueAt(table);
1496        String id = "0";
1497
1498        try {
1499            switch (match) {
1500                case ACCOUNT_PICK_TRASH_FOLDER:
1501                    return pickTrashFolder(uri);
1502                case ACCOUNT_PICK_SENT_FOLDER:
1503                    return pickSentFolder(uri);
1504                case UI_FOLDER:
1505                    return uiUpdateFolder(context, uri, values);
1506                case UI_RECENT_FOLDERS:
1507                    return uiUpdateRecentFolders(uri, values);
1508                case UI_DEFAULT_RECENT_FOLDERS:
1509                    return uiPopulateRecentFolders(uri);
1510                case UI_ATTACHMENT:
1511                    return uiUpdateAttachment(uri, values);
1512                case UI_MESSAGE:
1513                    return uiUpdateMessage(uri, values);
1514                case ACCOUNT_CHECK:
1515                    id = uri.getLastPathSegment();
1516                    // With any error, return 1 (a failure)
1517                    int res = 1;
1518                    Cursor ic = null;
1519                    try {
1520                        ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id});
1521                        if (ic.moveToFirst()) {
1522                            res = ic.getInt(0);
1523                        }
1524                    } finally {
1525                        if (ic != null) {
1526                            ic.close();
1527                        }
1528                    }
1529                    // Count of duplicated mailboxes
1530                    return res;
1531                case MESSAGE_SELECTION:
1532                    Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
1533                            selectionArgs, null, null, null);
1534                    try {
1535                        if (findCursor.moveToFirst()) {
1536                            return update(ContentUris.withAppendedId(
1537                                    Message.CONTENT_URI,
1538                                    findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
1539                                    values, null, null);
1540                        } else {
1541                            return 0;
1542                        }
1543                    } finally {
1544                        findCursor.close();
1545                    }
1546                case SYNCED_MESSAGE_ID:
1547                case UPDATED_MESSAGE_ID:
1548                case MESSAGE_ID:
1549                case BODY_ID:
1550                case ATTACHMENT_ID:
1551                case MAILBOX_ID:
1552                case ACCOUNT_ID:
1553                case HOSTAUTH_ID:
1554                case QUICK_RESPONSE_ID:
1555                case POLICY_ID:
1556                    id = uri.getPathSegments().get(1);
1557                    if (match == SYNCED_MESSAGE_ID) {
1558                        // For synced messages, first copy the old message to the updated table
1559                        // Note the insert or ignore semantics, guaranteeing that only the first
1560                        // update will be reflected in the updated message table; therefore this
1561                        // row will always have the "original" data
1562                        db.execSQL(UPDATED_MESSAGE_INSERT + id);
1563
1564                        Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
1565                        if (dstFolderId != null) {
1566                            addToMessageMove(db, id, dstFolderId);
1567                        }
1568                        Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
1569                        Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
1570                        int flagReadValue = (flagRead != null) ?
1571                                flagRead : MessageStateChange.VALUE_UNCHANGED;
1572                        int flagFavoriteValue = (flagFavorite != null) ?
1573                                flagFavorite : MessageStateChange.VALUE_UNCHANGED;
1574                        if (flagRead != null || flagFavorite != null) {
1575                            addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
1576                        }
1577                    } else if (match == MESSAGE_ID) {
1578                        db.execSQL(UPDATED_MESSAGE_DELETE + id);
1579                    }
1580                    result = db.update(tableName, values, whereWithId(id, selection),
1581                            selectionArgs);
1582                    if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
1583                        handleMessageUpdateNotifications(uri, id, values);
1584                    } else if (match == ATTACHMENT_ID) {
1585                        long attId = Integer.parseInt(id);
1586                        if (values.containsKey(Attachment.FLAGS)) {
1587                            int flags = values.getAsInteger(Attachment.FLAGS);
1588                            mAttachmentService.attachmentChanged(context, attId, flags);
1589                        }
1590                        // Notify UI if necessary; there are only two columns we can change that
1591                        // would be worth a notification
1592                        if (values.containsKey(AttachmentColumns.UI_STATE) ||
1593                                values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
1594                            // Notify on individual attachment
1595                            notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
1596                            Attachment att = Attachment.restoreAttachmentWithId(context, attId);
1597                            if (att != null) {
1598                                // And on owning Message
1599                                notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
1600                            }
1601                        }
1602                    } else if (match == MAILBOX_ID) {
1603                        notifyUIFolder(id, Mailbox.getAccountIdForMailbox(context, id));
1604                    } else if (match == ACCOUNT_ID) {
1605                        updateAccountSyncInterval(Long.parseLong(id), values);
1606                        // Notify individual account and "all accounts"
1607                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
1608                        resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
1609                    }
1610                    break;
1611                case BODY:
1612                case MESSAGE:
1613                case UPDATED_MESSAGE:
1614                case ATTACHMENT:
1615                case MAILBOX:
1616                case ACCOUNT:
1617                case HOSTAUTH:
1618                case POLICY:
1619                    result = db.update(tableName, values, selection, selectionArgs);
1620                    break;
1621
1622                case ACCOUNT_RESET_NEW_COUNT_ID:
1623                    id = uri.getPathSegments().get(1);
1624                    ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
1625                    if (values != null) {
1626                        Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME);
1627                        if (set != null) {
1628                            newMessageCount = new ContentValues();
1629                            newMessageCount.put(Account.NEW_MESSAGE_COUNT, set);
1630                        }
1631                    }
1632                    result = db.update(tableName, newMessageCount,
1633                            whereWithId(id, selection), selectionArgs);
1634                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1635                    break;
1636                case ACCOUNT_RESET_NEW_COUNT:
1637                    result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
1638                            selection, selectionArgs);
1639                    // Affects all accounts.  Just invalidate all account cache.
1640                    notificationUri = Account.CONTENT_URI; // Only notify account cursors.
1641                    break;
1642                case MESSAGE_MOVE:
1643                    result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs);
1644                    break;
1645                case MESSAGE_STATE_CHANGE:
1646                    result = db.update(MessageStateChange.TABLE_NAME, values, selection,
1647                            selectionArgs);
1648                    break;
1649                default:
1650                    throw new IllegalArgumentException("Unknown URI " + uri);
1651            }
1652        } catch (SQLiteException e) {
1653            checkDatabases();
1654            throw e;
1655        }
1656
1657        // Notify all notifier cursors
1658        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
1659
1660        resolver.notifyChange(notificationUri, null);
1661        return result;
1662    }
1663
1664    private void updateSyncStatus(final Bundle extras) {
1665        final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID);
1666        final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id);
1667        EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null);
1668    }
1669
1670    @Override
1671    public Bundle call(String method, String arg, Bundle extras) {
1672        LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg);
1673
1674        // Handle sync status callbacks.
1675        if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) {
1676            updateSyncStatus(extras);
1677            return null;
1678        }
1679
1680        // Handle send & save.
1681        final Uri accountUri = Uri.parse(arg);
1682        final long accountId = Long.parseLong(accountUri.getPathSegments().get(1));
1683
1684        Uri messageUri = null;
1685        if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) {
1686            messageUri = uiSendDraftMessage(accountId, extras);
1687            Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId);
1688        } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) {
1689            messageUri = uiSaveDraftMessage(accountId, extras);
1690        } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) {
1691            LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method);
1692        } else {
1693            LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method);
1694        }
1695
1696        final Bundle result;
1697        if (messageUri != null) {
1698            result = new Bundle(1);
1699            result.putParcelable(UIProvider.MessageColumns.URI, messageUri);
1700        } else {
1701            result = null;
1702        }
1703
1704        return result;
1705    }
1706
1707    @Override
1708    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
1709        if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
1710            LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
1711        }
1712
1713        final int match = findMatch(uri, "openFile");
1714        switch (match) {
1715            case ATTACHMENTS_CACHED_FILE_ACCESS:
1716                // Parse the cache file path out from the uri
1717                final String cachedFilePath =
1718                        uri.getQueryParameter(EmailContent.Attachment.CACHED_FILE_QUERY_PARAM);
1719
1720                if (cachedFilePath != null) {
1721                    // clearCallingIdentity means that the download manager will
1722                    // check our permissions rather than the permissions of whatever
1723                    // code is calling us.
1724                    long binderToken = Binder.clearCallingIdentity();
1725                    try {
1726                        LogUtils.d(TAG, "Opening attachment %s", cachedFilePath);
1727                        return ParcelFileDescriptor.open(
1728                                new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
1729                    } finally {
1730                        Binder.restoreCallingIdentity(binderToken);
1731                    }
1732                }
1733                break;
1734        }
1735
1736        throw new FileNotFoundException("unable to open file");
1737    }
1738
1739
1740    /**
1741     * Returns the base notification URI for the given content type.
1742     *
1743     * @param match The type of content that was modified.
1744     */
1745    private static Uri getBaseNotificationUri(int match) {
1746        Uri baseUri = null;
1747        switch (match) {
1748            case MESSAGE:
1749            case MESSAGE_ID:
1750            case SYNCED_MESSAGE_ID:
1751                baseUri = Message.NOTIFIER_URI;
1752                break;
1753            case ACCOUNT:
1754            case ACCOUNT_ID:
1755                baseUri = Account.NOTIFIER_URI;
1756                break;
1757        }
1758        return baseUri;
1759    }
1760
1761    /**
1762     * Sends a change notification to any cursors observers of the given base URI. The final
1763     * notification URI is dynamically built to contain the specified information. It will be
1764     * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
1765     * upon the given values.
1766     * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
1767     * If this is necessary, it can be added. However, due to the implementation of
1768     * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
1769     *
1770     * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
1771     * @param op Optional operation to be appended to the URI.
1772     * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
1773     *           appended to the base URI.
1774     */
1775    private void sendNotifierChange(Uri baseUri, String op, String id) {
1776        if (baseUri == null) return;
1777
1778        final ContentResolver resolver = getContext().getContentResolver();
1779
1780        // Append the operation, if specified
1781        if (op != null) {
1782            baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
1783        }
1784
1785        long longId = 0L;
1786        try {
1787            longId = Long.valueOf(id);
1788        } catch (NumberFormatException ignore) {}
1789        if (longId > 0) {
1790            resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null);
1791        } else {
1792            resolver.notifyChange(baseUri, null);
1793        }
1794
1795        // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
1796        if (baseUri.equals(Message.NOTIFIER_URI)) {
1797            sendMessageListDataChangedNotification();
1798        }
1799    }
1800
1801    private void sendMessageListDataChangedNotification() {
1802        final Context context = getContext();
1803        final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
1804        // Ideally this intent would contain information about which account changed, to limit the
1805        // updates to that particular account.  Unfortunately, that information is not available in
1806        // sendNotifierChange().
1807        context.sendBroadcast(intent);
1808    }
1809
1810    @Override
1811    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1812            throws OperationApplicationException {
1813        Context context = getContext();
1814        SQLiteDatabase db = getDatabase(context);
1815        db.beginTransaction();
1816        try {
1817            ContentProviderResult[] results = super.applyBatch(operations);
1818            db.setTransactionSuccessful();
1819            return results;
1820        } finally {
1821            db.endTransaction();
1822        }
1823    }
1824
1825    public static interface AttachmentService {
1826        /**
1827         * Notify the service that an attachment has changed.
1828         */
1829        void attachmentChanged(Context context, long id, int flags);
1830    }
1831
1832    private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() {
1833        @Override
1834        public void attachmentChanged(Context context, long id, int flags) {
1835            // The default implementation delegates to the real service.
1836            AttachmentDownloadService.attachmentChanged(context, id, flags);
1837        }
1838    };
1839    private final AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
1840
1841    private Cursor notificationQuery(final Uri uri) {
1842        final SQLiteDatabase db = getDatabase(getContext());
1843        final String accountId = uri.getLastPathSegment();
1844
1845        final StringBuilder sqlBuilder = new StringBuilder();
1846        sqlBuilder.append("SELECT ");
1847        sqlBuilder.append(MessageColumns.MAILBOX_KEY).append(", ");
1848        sqlBuilder.append("SUM(CASE ")
1849                .append(MessageColumns.FLAG_READ).append(" WHEN 0 THEN 1 ELSE 0 END), ");
1850        sqlBuilder.append("SUM(CASE ")
1851                .append(MessageColumns.FLAG_SEEN).append(" WHEN 0 THEN 1 ELSE 0 END)\n");
1852        sqlBuilder.append("FROM ");
1853        sqlBuilder.append(Message.TABLE_NAME).append('\n');
1854        sqlBuilder.append("WHERE ");
1855        sqlBuilder.append(MessageColumns.ACCOUNT_KEY).append(" = ?\n");
1856        sqlBuilder.append("GROUP BY ");
1857        sqlBuilder.append(MessageColumns.MAILBOX_KEY);
1858
1859        final String sql = sqlBuilder.toString();
1860
1861        final String[] selectionArgs = {accountId};
1862
1863        return db.rawQuery(sql, selectionArgs);
1864    }
1865
1866    public Cursor mostRecentMessageQuery(Uri uri) {
1867        SQLiteDatabase db = getDatabase(getContext());
1868        String mailboxId = uri.getLastPathSegment();
1869        return db.rawQuery("select max(_id) from Message where mailboxKey=?",
1870                new String[] {mailboxId});
1871    }
1872
1873    private Cursor getMailboxMessageCount(Uri uri) {
1874        SQLiteDatabase db = getDatabase(getContext());
1875        String mailboxId = uri.getLastPathSegment();
1876        return db.rawQuery("select count(*) from Message where mailboxKey=?",
1877                new String[] {mailboxId});
1878    }
1879
1880    /**
1881     * Support for UnifiedEmail below
1882     */
1883
1884    private static final String NOT_A_DRAFT_STRING =
1885        Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
1886
1887    private static final String CONVERSATION_FLAGS =
1888            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
1889                ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE +
1890                " ELSE 0 END + " +
1891            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED +
1892                ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED +
1893                " ELSE 0 END + " +
1894             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO +
1895                ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED +
1896                " ELSE 0 END";
1897
1898    /**
1899     * Array of pre-defined account colors (legacy colors from old email app)
1900     */
1901    private static final int[] ACCOUNT_COLORS = new int[] {
1902        0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79,
1903        0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4
1904    };
1905
1906    private static final String CONVERSATION_COLOR =
1907            "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length +
1908                    " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
1909                    " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
1910                    " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
1911                    " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
1912                    " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
1913                    " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
1914                    " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
1915                    " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
1916                    " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
1917            " END";
1918
1919    private static final String ACCOUNT_COLOR =
1920            "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length +
1921                    " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
1922                    " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
1923                    " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
1924                    " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
1925                    " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
1926                    " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
1927                    " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
1928                    " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
1929                    " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
1930            " END";
1931    /**
1932     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
1933     * conversation list in UnifiedEmail)
1934     */
1935    private static ProjectionMap getMessageListMap() {
1936        if (sMessageListMap == null) {
1937            sMessageListMap = ProjectionMap.builder()
1938                .add(BaseColumns._ID, MessageColumns.ID)
1939                .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
1940                .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
1941                .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
1942                .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
1943                .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null)
1944                .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
1945                .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
1946                .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
1947                .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
1948                .add(UIProvider.ConversationColumns.SENDING_STATE,
1949                        Integer.toString(ConversationSendingState.OTHER))
1950                .add(UIProvider.ConversationColumns.PRIORITY,
1951                        Integer.toString(ConversationPriority.LOW))
1952                .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
1953                .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN)
1954                .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
1955                .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS)
1956                .add(UIProvider.ConversationColumns.ACCOUNT_URI,
1957                        uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
1958                .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
1959                .build();
1960        }
1961        return sMessageListMap;
1962    }
1963    private static ProjectionMap sMessageListMap;
1964
1965    /**
1966     * Generate UIProvider draft type; note the test for "reply all" must come before "reply"
1967     */
1968    private static final String MESSAGE_DRAFT_TYPE =
1969        "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL +
1970            ") !=0 THEN " + UIProvider.DraftType.COMPOSE +
1971        " WHEN (" + MessageColumns.FLAGS + "&" + (1<<20) +
1972            ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL +
1973        " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY +
1974            ") !=0 THEN " + UIProvider.DraftType.REPLY +
1975        " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD +
1976            ") !=0 THEN " + UIProvider.DraftType.FORWARD +
1977            " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END";
1978
1979    private static final String MESSAGE_FLAGS =
1980            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
1981            ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE +
1982            " ELSE 0 END";
1983
1984    /**
1985     * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
1986     * UnifiedEmail
1987     */
1988    private static ProjectionMap getMessageViewMap() {
1989        if (sMessageViewMap == null) {
1990            sMessageViewMap = ProjectionMap.builder()
1991                .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID)
1992                .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
1993                .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
1994                .add(UIProvider.MessageColumns.CONVERSATION_ID,
1995                        uriWithFQId("uimessage", Message.TABLE_NAME))
1996                .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT)
1997                .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET)
1998                .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST)
1999                .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST)
2000                .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST)
2001                .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST)
2002                .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST)
2003                .add(UIProvider.MessageColumns.DATE_RECEIVED_MS,
2004                        EmailContent.MessageColumns.TIMESTAMP)
2005                .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT)
2006                .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT)
2007                .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
2008                .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
2009                .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
2010                .add(UIProvider.MessageColumns.HAS_ATTACHMENTS,
2011                        EmailContent.MessageColumns.FLAG_ATTACHMENT)
2012                .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
2013                        uriWithFQId("uiattachments", Message.TABLE_NAME))
2014                .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS)
2015                .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE)
2016                .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI,
2017                        uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
2018                .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE)
2019                .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ)
2020                .add(UIProvider.MessageColumns.SEEN, EmailContent.MessageColumns.FLAG_SEEN)
2021                .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null)
2022                .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL,
2023                        Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING))
2024                .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE,
2025                        Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK))
2026                .add(UIProvider.MessageColumns.VIA_DOMAIN, null)
2027                .build();
2028        }
2029        return sMessageViewMap;
2030    }
2031    private static ProjectionMap sMessageViewMap;
2032
2033    /**
2034     * Generate UIProvider folder capabilities from mailbox flags
2035     */
2036    private static final String FOLDER_CAPABILITIES =
2037        "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
2038            ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
2039            " ELSE 0 END";
2040
2041    /**
2042     * Convert EmailProvider type to UIProvider type
2043     */
2044    private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE
2045            + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + UIProvider.FolderType.INBOX
2046            + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + UIProvider.FolderType.DRAFT
2047            + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + UIProvider.FolderType.OUTBOX
2048            + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + UIProvider.FolderType.SENT
2049            + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + UIProvider.FolderType.TRASH
2050            + " WHEN " + Mailbox.TYPE_JUNK    + " THEN " + UIProvider.FolderType.SPAM
2051            + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED
2052            + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD
2053            + " WHEN " + Mailbox.TYPE_SEARCH + " THEN "
2054                    + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH)
2055            + " ELSE " + UIProvider.FolderType.DEFAULT + " END";
2056
2057    private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE
2058            + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + R.drawable.ic_folder_inbox
2059            + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + R.drawable.ic_folder_drafts
2060            + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + R.drawable.ic_folder_outbox
2061            + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + R.drawable.ic_folder_sent
2062            + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + R.drawable.ic_folder_trash
2063            + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_folder_star
2064            + " ELSE -1 END";
2065
2066    /**
2067     * Local-only folders set totalCount < 0; such folders should substitute message count for
2068     * total count.
2069     * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types.
2070     */
2071    private static final String TOTAL_COUNT = "CASE WHEN "
2072            + MailboxColumns.TOTAL_COUNT + "<0 OR "
2073            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR "
2074            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR "
2075            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH
2076            + " THEN " + MailboxColumns.MESSAGE_COUNT
2077            + " ELSE " + MailboxColumns.TOTAL_COUNT + " END";
2078
2079    private static ProjectionMap getFolderListMap() {
2080        if (sFolderListMap == null) {
2081            sFolderListMap = ProjectionMap.builder()
2082                .add(BaseColumns._ID, MailboxColumns.ID)
2083                .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID)
2084                .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
2085                .add(UIProvider.FolderColumns.NAME, "displayName")
2086                .add(UIProvider.FolderColumns.HAS_CHILDREN,
2087                        MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
2088                .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES)
2089                .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
2090                .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
2091                .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
2092                .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
2093                .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT)
2094                .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH))
2095                .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
2096                .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
2097                .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE)
2098                .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON)
2099                .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME)
2100                .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY
2101                        + "=" + Mailbox.NO_MAILBOX + " then NULL else " +
2102                        uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end")
2103                .build();
2104        }
2105        return sFolderListMap;
2106    }
2107    private static ProjectionMap sFolderListMap;
2108
2109    /**
2110     * Constructs the map of default entries for accounts. These values can be overridden in
2111     * {@link #genQueryAccount(String[], String)}.
2112     */
2113    private static ProjectionMap getAccountListMap(Context context) {
2114        if (sAccountListMap == null) {
2115            final MailPrefs mailPrefs = MailPrefs.get(context);
2116
2117            final ProjectionMap.Builder builder = ProjectionMap.builder()
2118                    .add(BaseColumns._ID, AccountColumns.ID)
2119                    .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
2120                    .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders"))
2121                    .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
2122                    .add(UIProvider.AccountColumns.UNDO_URI,
2123                            ("'content://" + EmailContent.AUTHORITY + "/uiundo'"))
2124                    .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
2125                    .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
2126                            // TODO: Is provider version used?
2127                    .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
2128                    .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
2129                    .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI,
2130                            uriWithId("uirecentfolders"))
2131                    .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
2132                            uriWithId("uidefaultrecentfolders"))
2133                    .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE,
2134                            AccountColumns.SIGNATURE)
2135                    .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS,
2136                            Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
2137                    .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR,
2138                            Integer.toString(mailPrefs.getDefaultReplyAll()
2139                                    ? UIProvider.DefaultReplyBehavior.REPLY_ALL
2140                                    : UIProvider.DefaultReplyBehavior.REPLY))
2141                    .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0")
2142                    .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
2143                            Integer.toString(UIProvider.ConversationViewMode.UNDEFINED))
2144                    .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null);
2145
2146            final String feedbackUri = context.getString(R.string.email_feedback_uri);
2147            if (!TextUtils.isEmpty(feedbackUri)) {
2148                // This string needs to be in single quotes, as it will be used as a constant
2149                // in a sql expression
2150                builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI,
2151                        "'" + feedbackUri + "'");
2152            }
2153
2154            sAccountListMap = builder.build();
2155        }
2156        return sAccountListMap;
2157    }
2158    private static ProjectionMap sAccountListMap;
2159
2160    private static ProjectionMap getQuickResponseMap() {
2161        if (sQuickResponseMap == null) {
2162            sQuickResponseMap = ProjectionMap.builder()
2163                    .add(UIProvider.QuickResponseColumns.TEXT,
2164                            EmailContent.QuickResponseColumns.TEXT)
2165                    .add(UIProvider.QuickResponseColumns.URI,
2166                            "'" + combinedUriString("quickresponse", "") + "'||"
2167                                    + EmailContent.QuickResponseColumns.ID)
2168                    .build();
2169        }
2170        return sQuickResponseMap;
2171    }
2172    private static ProjectionMap sQuickResponseMap;
2173
2174    /**
2175     * The "ORDER BY" clause for top level folders
2176     */
2177    private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
2178        + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
2179        + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
2180        + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
2181        + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
2182        + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
2183        + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
2184        // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
2185        + " ELSE 10 END"
2186        + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
2187
2188    /**
2189     * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
2190     */
2191    private static ProjectionMap getAttachmentMap() {
2192        if (sAttachmentMap == null) {
2193            sAttachmentMap = ProjectionMap.builder()
2194                .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
2195                .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
2196                .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
2197                .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
2198                .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
2199                .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
2200                .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE,
2201                        AttachmentColumns.UI_DOWNLOADED_SIZE)
2202                .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
2203                .build();
2204        }
2205        return sAttachmentMap;
2206    }
2207    private static ProjectionMap sAttachmentMap;
2208
2209    /**
2210     * Generate the SELECT clause using a specified mapping and the original UI projection
2211     * @param map the ProjectionMap to use for this projection
2212     * @param projection the projection as sent by UnifiedEmail
2213     * @return a StringBuilder containing the SELECT expression for a SQLite query
2214     */
2215    private static StringBuilder genSelect(ProjectionMap map, String[] projection) {
2216        return genSelect(map, projection, EMPTY_CONTENT_VALUES);
2217    }
2218
2219    private static StringBuilder genSelect(ProjectionMap map, String[] projection,
2220            ContentValues values) {
2221        final StringBuilder sb = new StringBuilder("SELECT ");
2222        boolean first = true;
2223        for (final String column: projection) {
2224            if (first) {
2225                first = false;
2226            } else {
2227                sb.append(',');
2228            }
2229            final String val;
2230            // First look at values; this is an override of default behavior
2231            if (values.containsKey(column)) {
2232                final String value = values.getAsString(column);
2233                if (value == null) {
2234                    val = "NULL AS " + column;
2235                } else if (value.startsWith("@")) {
2236                    val = value.substring(1) + " AS " + column;
2237                } else {
2238                    val = "'" + value + "' AS " + column;
2239                }
2240            } else {
2241                // Now, get the standard value for the column from our projection map
2242                final String mapVal = map.get(column);
2243                // If we don't have the column, return "NULL AS <column>", and warn
2244                if (mapVal == null) {
2245                    val = "NULL AS " + column;
2246                    // Apparently there's a lot of these, so don't spam the log with warnings
2247                    // LogUtils.w(TAG, "column " + column + " missing from projection map");
2248                } else {
2249                    val = mapVal;
2250                }
2251            }
2252            sb.append(val);
2253        }
2254        return sb;
2255    }
2256
2257    /**
2258     * Convenience method to create a Uri string given the "type" of query; we append the type
2259     * of the query and the id column name (_id)
2260     *
2261     * @param type the "type" of the query, as defined by our UriMatcher definitions
2262     * @return a Uri string
2263     */
2264    private static String uriWithId(String type) {
2265        return uriWithColumn(type, EmailContent.RECORD_ID);
2266    }
2267
2268    /**
2269     * Convenience method to create a Uri string given the "type" of query; we append the type
2270     * of the query and the passed in column name
2271     *
2272     * @param type the "type" of the query, as defined by our UriMatcher definitions
2273     * @param columnName the column in the table being queried
2274     * @return a Uri string
2275     */
2276    private static String uriWithColumn(String type, String columnName) {
2277        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName;
2278    }
2279
2280    /**
2281     * Convenience method to create a Uri string given the "type" of query and the table name to
2282     * which it applies; we append the type of the query and the fully qualified (FQ) id column
2283     * (i.e. including the table name); we need this for join queries where _id would otherwise
2284     * be ambiguous
2285     *
2286     * @param type the "type" of the query, as defined by our UriMatcher definitions
2287     * @param tableName the name of the table whose _id is referred to
2288     * @return a Uri string
2289     */
2290    private static String uriWithFQId(String type, String tableName) {
2291        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
2292    }
2293
2294    // Regex that matches start of img tag. '<(?i)img\s+'.
2295    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
2296
2297    /**
2298     * Class that holds the sqlite query and the attachment (JSON) value (which might be null)
2299     */
2300    private static class MessageQuery {
2301        final String query;
2302        final String attachmentJson;
2303
2304        MessageQuery(String _query, String _attachmentJson) {
2305            query = _query;
2306            attachmentJson = _attachmentJson;
2307        }
2308    }
2309
2310    /**
2311     * Generate the "view message" SQLite query, given a projection from UnifiedEmail
2312     *
2313     * @param uiProjection as passed from UnifiedEmail
2314     * @return the SQLite query to be executed on the EmailProvider database
2315     */
2316    private MessageQuery genQueryViewMessage(String[] uiProjection, String id) {
2317        Context context = getContext();
2318        long messageId = Long.parseLong(id);
2319        Message msg = Message.restoreMessageWithId(context, messageId);
2320        ContentValues values = new ContentValues();
2321        String attachmentJson = null;
2322        if (msg != null) {
2323            Body body = Body.restoreBodyWithMessageId(context, messageId);
2324            if (body != null) {
2325                if (body.mHtmlContent != null) {
2326                    if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) {
2327                        values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1);
2328                    }
2329                }
2330            }
2331            Address[] fromList = Address.unpack(msg.mFrom);
2332            int autoShowImages = 0;
2333            final MailPrefs mailPrefs = MailPrefs.get(context);
2334            for (Address sender : fromList) {
2335                final String email = sender.getAddress();
2336                if (mailPrefs.getDisplayImagesFromSender(email)) {
2337                    autoShowImages = 1;
2338                    break;
2339                }
2340            }
2341            values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages);
2342            // Add attachments...
2343            Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
2344            if (atts.length > 0) {
2345                ArrayList<com.android.mail.providers.Attachment> uiAtts =
2346                        new ArrayList<com.android.mail.providers.Attachment>();
2347                for (Attachment att : atts) {
2348                    if (att.mContentId != null && att.getContentUri() != null) {
2349                        continue;
2350                    }
2351                    com.android.mail.providers.Attachment uiAtt =
2352                            new com.android.mail.providers.Attachment();
2353                    uiAtt.setName(att.mFileName);
2354                    uiAtt.setContentType(att.mMimeType);
2355                    uiAtt.size = (int) att.mSize;
2356                    uiAtt.uri = uiUri("uiattachment", att.mId);
2357                    uiAtts.add(uiAtt);
2358                }
2359                values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal
2360                attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts);
2361            }
2362            if (msg.mDraftInfo != 0) {
2363                values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT,
2364                        (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0);
2365                values.put(UIProvider.MessageColumns.QUOTE_START_POS,
2366                        msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK);
2367            }
2368            if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
2369                values.put(UIProvider.MessageColumns.EVENT_INTENT_URI,
2370                        "content://ui.email2.android.com/event/" + msg.mId);
2371            }
2372        }
2373        StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values);
2374        sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + " ON " +
2375                Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " WHERE " +
2376                Message.TABLE_NAME + "." + Message.RECORD_ID + "=?");
2377        String sql = sb.toString();
2378        return new MessageQuery(sql, attachmentJson);
2379    }
2380
2381    /**
2382     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
2383     *
2384     * @param uiProjection as passed from UnifiedEmail
2385     * @param unseenOnly <code>true</code> to only return unseen messages
2386     * @return the SQLite query to be executed on the EmailProvider database
2387     */
2388    private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) {
2389        StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
2390        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
2391                Message.FLAG_LOADED_SELECTION + " AND " +
2392                Message.MAILBOX_KEY + "=? ");
2393        if (unseenOnly) {
2394            sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 ");
2395        }
2396        sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC ");
2397        sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT);
2398        return sb.toString();
2399    }
2400
2401    /**
2402     * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail
2403     *
2404     * @param uiProjection as passed from UnifiedEmail
2405     * @param mailboxId the id of the virtual mailbox
2406     * @param unseenOnly <code>true</code> to only return unseen messages
2407     * @return the SQLite query to be executed on the EmailProvider database
2408     */
2409    private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection,
2410            long mailboxId, final boolean unseenOnly) {
2411        ContentValues values = new ContentValues();
2412        values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR);
2413        final int virtualMailboxId = getVirtualMailboxType(mailboxId);
2414        final String[] selectionArgs;
2415        StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values);
2416        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
2417                Message.FLAG_LOADED_SELECTION + " AND ");
2418        if (isCombinedMailbox(mailboxId)) {
2419            if (unseenOnly) {
2420                sb.append(MessageColumns.FLAG_SEEN).append("=0 AND ");
2421            }
2422            selectionArgs = null;
2423        } else {
2424            if (virtualMailboxId == Mailbox.TYPE_INBOX) {
2425                throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
2426            }
2427            sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND ");
2428            selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)};
2429        }
2430        switch (getVirtualMailboxType(mailboxId)) {
2431            case Mailbox.TYPE_INBOX:
2432                sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID +
2433                        " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE +
2434                        "=" + Mailbox.TYPE_INBOX + ")");
2435                break;
2436            case Mailbox.TYPE_STARRED:
2437                sb.append(MessageColumns.FLAG_FAVORITE + "=1");
2438                break;
2439            case Mailbox.TYPE_UNREAD:
2440                sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY +
2441                        " NOT IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME +
2442                        " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")");
2443                break;
2444            default:
2445                throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
2446        }
2447        sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC");
2448        return db.rawQuery(sb.toString(), selectionArgs);
2449    }
2450
2451    /**
2452     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
2453     *
2454     * @param uiProjection as passed from UnifiedEmail
2455     * @return the SQLite query to be executed on the EmailProvider database
2456     */
2457    private static String genQueryConversation(String[] uiProjection) {
2458        StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
2459        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?");
2460        return sb.toString();
2461    }
2462
2463    /**
2464     * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
2465     *
2466     * @param uiProjection as passed from UnifiedEmail
2467     * @return the SQLite query to be executed on the EmailProvider database
2468     */
2469    private static String genQueryAccountMailboxes(String[] uiProjection) {
2470        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
2471        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
2472                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
2473                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
2474                " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
2475        sb.append(MAILBOX_ORDER_BY);
2476        return sb.toString();
2477    }
2478
2479    /**
2480     * Generate the "all folders" SQLite query, given a projection from UnifiedEmail.  The list is
2481     * sorted by the name as it appears in a hierarchical listing
2482     *
2483     * @param uiProjection as passed from UnifiedEmail
2484     * @return the SQLite query to be executed on the EmailProvider database
2485     */
2486    private static String genQueryAccountAllMailboxes(String[] uiProjection) {
2487        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
2488        // Use a derived column to choose either hierarchicalName or displayName
2489        sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " +
2490                MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME +
2491                " end as h_name");
2492        // Order by the derived column
2493        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
2494                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
2495                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
2496                " ORDER BY h_name");
2497        return sb.toString();
2498    }
2499
2500    /**
2501     * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail
2502     *
2503     * @param uiProjection as passed from UnifiedEmail
2504     * @return the SQLite query to be executed on the EmailProvider database
2505     */
2506    private static String genQueryRecentMailboxes(String[] uiProjection) {
2507        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
2508        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
2509                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
2510                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
2511                " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " +
2512                MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " +
2513                MailboxColumns.LAST_TOUCHED_TIME + " DESC");
2514        return sb.toString();
2515    }
2516
2517    private static int getFolderCapabilities(EmailServiceInfo info, int flags, int type,
2518            long mailboxId) {
2519        // All folders support delete, except drafts.
2520        int caps = 0;
2521        if (type != Mailbox.TYPE_DRAFTS) {
2522            caps = UIProvider.FolderCapabilities.DELETE;
2523        }
2524        if (info != null && info.offerLookback) {
2525            // Protocols supporting lookback support settings
2526            caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS;
2527        }
2528        if ((flags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) {
2529            // If the mailbox can accept moved mail, report that as well
2530            caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES;
2531            caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION;
2532        }
2533
2534        // For trash, we don't allow undo
2535        if (type == Mailbox.TYPE_TRASH) {
2536            caps =  UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES |
2537                    UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION |
2538                    UIProvider.FolderCapabilities.DELETE |
2539                    UIProvider.FolderCapabilities.DELETE_ACTION_FINAL;
2540        }
2541        if (isVirtualMailbox(mailboxId)) {
2542            caps |= UIProvider.FolderCapabilities.IS_VIRTUAL;
2543        }
2544        return caps;
2545    }
2546
2547    /**
2548     * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
2549     *
2550     * @param uiProjection as passed from UnifiedEmail
2551     * @return the SQLite query to be executed on the EmailProvider database
2552     */
2553    private String genQueryMailbox(String[] uiProjection, String id) {
2554        long mailboxId = Long.parseLong(id);
2555        ContentValues values = new ContentValues();
2556        if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
2557            // This is the current search mailbox; use the total count
2558            values = new ContentValues();
2559            values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount);
2560            // "load more" is valid for search results
2561            values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2562                    uiUriString("uiloadmore", mailboxId));
2563            values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE);
2564        } else {
2565            Context context = getContext();
2566            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
2567            // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot)
2568            if (mailbox != null) {
2569                String protocol = Account.getProtocol(context, mailbox.mAccountKey);
2570                EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
2571                // All folders support delete
2572                if (info != null && info.offerLoadMore) {
2573                    // "load more" is valid for protocols not supporting "lookback"
2574                    values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
2575                            uiUriString("uiloadmore", mailboxId));
2576                }
2577                values.put(UIProvider.FolderColumns.CAPABILITIES,
2578                        getFolderCapabilities(info, mailbox.mFlags, mailbox.mType, mailboxId));
2579                // The persistent id is used to form a filename, so we must ensure that it doesn't
2580                // include illegal characters (such as '/'). Only perform the encoding if this
2581                // query wants the persistent id.
2582                boolean shouldEncodePersistentId = false;
2583                if (uiProjection == null) {
2584                    shouldEncodePersistentId = true;
2585                } else {
2586                    for (final String column : uiProjection) {
2587                        if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) {
2588                            shouldEncodePersistentId = true;
2589                            break;
2590                        }
2591                    }
2592                }
2593                if (shouldEncodePersistentId) {
2594                    values.put(UIProvider.FolderColumns.PERSISTENT_ID,
2595                            Base64.encodeToString(mailbox.mServerId.getBytes(),
2596                                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
2597                }
2598             }
2599        }
2600        StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values);
2601        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?");
2602        return sb.toString();
2603    }
2604
2605    public static final String LEGACY_AUTHORITY = "ui.email.android.com";
2606    private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY);
2607
2608    private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
2609
2610    private static String getExternalUriString(String segment, String account) {
2611        return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
2612                .appendQueryParameter("account", account).build().toString();
2613    }
2614
2615    private static String getExternalUriStringEmail2(String segment, String account) {
2616        return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
2617                .appendQueryParameter("account", account).build().toString();
2618    }
2619
2620    private static String getBits(int bitField) {
2621        StringBuilder sb = new StringBuilder(" ");
2622        for (int i = 0; i < 32; i++, bitField >>= 1) {
2623            if ((bitField & 1) != 0) {
2624                sb.append(i)
2625                        .append(" ");
2626            }
2627        }
2628        return sb.toString();
2629    }
2630
2631    private static int getCapabilities(Context context, long accountId) {
2632        final EmailServiceProxy service =
2633                EmailServiceUtils.getServiceForAccount(context, accountId);
2634        int capabilities = 0;
2635        Account acct = null;
2636        try {
2637            service.setTimeout(10);
2638            acct = Account.restoreAccountWithId(context, accountId);
2639            if (acct == null) {
2640                LogUtils.d(TAG, "getCapabilities() for " + accountId
2641                        + ": returning 0x0 (no account)");
2642                return 0;
2643            }
2644            capabilities = service.getCapabilities(acct);
2645            LogUtils.d(TAG, "getCapabilities() for %s: 0x%x %s",
2646                    LogUtils.sanitizeAccountName(acct.mDisplayName),
2647                    capabilities, getBits(capabilities));
2648       } catch (RemoteException e) {
2649            // Nothing to do
2650           LogUtils.w(TAG, "getCapabilities() for " + acct.mDisplayName + ": RemoteException");
2651        }
2652
2653        // If the configuration states that feedback is supported, add that capability
2654        final Resources res = context.getResources();
2655        if (res.getBoolean(R.bool.feedback_supported)) {
2656            capabilities |= UIProvider.AccountCapabilities.SEND_FEEDBACK;
2657        }
2658        return capabilities;
2659    }
2660
2661    /**
2662     * Generate a "single account" SQLite query, given a projection from UnifiedEmail
2663     *
2664     * @param uiProjection as passed from UnifiedEmail
2665     * @param id account row ID
2666     * @return the SQLite query to be executed on the EmailProvider database
2667     */
2668    private String genQueryAccount(String[] uiProjection, String id) {
2669        final ContentValues values = new ContentValues();
2670        final long accountId = Long.parseLong(id);
2671        final Context context = getContext();
2672
2673        EmailServiceInfo info = null;
2674
2675        // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null.
2676        final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection);
2677
2678        if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) {
2679            // Get account capabilities from the service
2680            values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId));
2681        }
2682        if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
2683            values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
2684                    getExternalUriString("settings", id));
2685        }
2686        if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) {
2687            values.put(UIProvider.AccountColumns.COMPOSE_URI,
2688                    getExternalUriStringEmail2("compose", id));
2689        }
2690        if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) {
2691            values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE);
2692        }
2693        if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) {
2694            values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR);
2695        }
2696
2697        final Preferences prefs = Preferences.getPreferences(getContext());
2698        final MailPrefs mailPrefs = MailPrefs.get(getContext());
2699        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
2700            values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE,
2701                    prefs.getConfirmDelete() ? "1" : "0");
2702        }
2703        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
2704            values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND,
2705                    prefs.getConfirmSend() ? "1" : "0");
2706        }
2707        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) {
2708            values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE,
2709                    mailPrefs.getConversationListSwipeActionInteger(false));
2710        }
2711        if (projectionColumns.contains(
2712                UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
2713            values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON,
2714                    getConversationListIcon(mailPrefs));
2715        }
2716        if (projectionColumns.contains(
2717                UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) {
2718            values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS,
2719                    "0");
2720        }
2721        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
2722            int autoAdvance = prefs.getAutoAdvanceDirection();
2723            values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE,
2724                    autoAdvanceToUiValue(autoAdvance));
2725        }
2726        if (projectionColumns.contains(
2727                UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) {
2728            int textZoom = prefs.getTextZoom();
2729            values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE,
2730                    textZoomToUiValue(textZoom));
2731        }
2732        // Set default inbox, if we've got an inbox; otherwise, say initial sync needed
2733        final long inboxMailboxId =
2734                Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
2735        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) &&
2736                inboxMailboxId != Mailbox.NO_MAILBOX) {
2737            values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
2738                    uiUriString("uifolder", inboxMailboxId));
2739        }
2740        if (projectionColumns.contains(
2741                UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) &&
2742                inboxMailboxId != Mailbox.NO_MAILBOX) {
2743            values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME,
2744                    Mailbox.getDisplayName(context, inboxMailboxId));
2745        }
2746        if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) {
2747            if (inboxMailboxId != Mailbox.NO_MAILBOX) {
2748                values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
2749            } else {
2750                values.put(UIProvider.AccountColumns.SYNC_STATUS,
2751                        UIProvider.SyncStatus.INITIAL_SYNC_NEEDED);
2752            }
2753        }
2754        if (projectionColumns.contains(
2755                UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) {
2756            // Email doesn't support priority inbox, so always state priority arrows disabled.
2757            values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0");
2758        }
2759        if (projectionColumns.contains(
2760                UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) {
2761            // Set the setup intent if needed
2762            // TODO We should clarify/document the trash/setup relationship
2763            long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH);
2764            if (trashId == Mailbox.NO_MAILBOX) {
2765                info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
2766                if (info != null && info.requiresSetup) {
2767                    values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI,
2768                            getExternalUriString("setup", id));
2769                }
2770            }
2771        }
2772        if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) {
2773            final String type;
2774            if (info == null) {
2775                info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
2776            }
2777            if (info != null) {
2778                type = info.accountType;
2779            } else {
2780                type = "unknown";
2781            }
2782
2783            values.put(UIProvider.AccountColumns.TYPE, type);
2784        }
2785        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) &&
2786                inboxMailboxId != Mailbox.NO_MAILBOX) {
2787            values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX,
2788                    uiUriString("uifolder", inboxMailboxId));
2789        }
2790        if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) {
2791            values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY);
2792        }
2793        if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) {
2794            values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI,
2795                    combinedUriString("quickresponse/account", id));
2796        }
2797
2798        final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values);
2799        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?");
2800        return sb.toString();
2801    }
2802
2803    private static int autoAdvanceToUiValue(int autoAdvance) {
2804        switch(autoAdvance) {
2805            case Preferences.AUTO_ADVANCE_OLDER:
2806                return UIProvider.AutoAdvance.OLDER;
2807            case Preferences.AUTO_ADVANCE_NEWER:
2808                return UIProvider.AutoAdvance.NEWER;
2809            case Preferences.AUTO_ADVANCE_MESSAGE_LIST:
2810            default:
2811                return UIProvider.AutoAdvance.LIST;
2812        }
2813    }
2814
2815    private static int textZoomToUiValue(int textZoom) {
2816        switch(textZoom) {
2817            case Preferences.TEXT_ZOOM_HUGE:
2818                return UIProvider.MessageTextSize.HUGE;
2819            case Preferences.TEXT_ZOOM_LARGE:
2820                return UIProvider.MessageTextSize.LARGE;
2821            case Preferences.TEXT_ZOOM_NORMAL:
2822                return UIProvider.MessageTextSize.NORMAL;
2823            case Preferences.TEXT_ZOOM_SMALL:
2824                return UIProvider.MessageTextSize.SMALL;
2825            case Preferences.TEXT_ZOOM_TINY:
2826                return UIProvider.MessageTextSize.TINY;
2827            default:
2828                return UIProvider.MessageTextSize.NORMAL;
2829        }
2830    }
2831
2832    /**
2833     * Generate a Uri string for a combined mailbox uri
2834     * @param type the uri command type (e.g. "uimessages")
2835     * @param id the id of the item (e.g. an account, mailbox, or message id)
2836     * @return a Uri string
2837     */
2838    private static String combinedUriString(String type, String id) {
2839        return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id;
2840    }
2841
2842    public static final long COMBINED_ACCOUNT_ID = 0x10000000;
2843
2844    /**
2845     * Generate an id for a combined mailbox of a given type
2846     * @param type the mailbox type for the combined mailbox
2847     * @return the id, as a String
2848     */
2849    private static String combinedMailboxId(int type) {
2850        return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type);
2851    }
2852
2853    public static long getVirtualMailboxId(long accountId, int type) {
2854        return (accountId << 32) + type;
2855    }
2856
2857    private static boolean isVirtualMailbox(long mailboxId) {
2858        return mailboxId >= 0x100000000L;
2859    }
2860
2861    private static boolean isCombinedMailbox(long mailboxId) {
2862        return (mailboxId >> 32) == COMBINED_ACCOUNT_ID;
2863    }
2864
2865    private static long getVirtualMailboxAccountId(long mailboxId) {
2866        return mailboxId >> 32;
2867    }
2868
2869    private static String getVirtualMailboxAccountIdString(long mailboxId) {
2870        return Long.toString(mailboxId >> 32);
2871    }
2872
2873    private static int getVirtualMailboxType(long mailboxId) {
2874        return (int)(mailboxId & 0xF);
2875    }
2876
2877    private void addCombinedAccountRow(MatrixCursor mc) {
2878        final long lastUsedAccountId =
2879                Preferences.getPreferences(getContext()).getLastUsedAccountId();
2880        final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId);
2881        if (id == Account.NO_ACCOUNT) return;
2882
2883        // Build a map of the requested columns to the appropriate positions
2884        final ImmutableMap.Builder<String, Integer> builder =
2885                new ImmutableMap.Builder<String, Integer>();
2886        final String[] columnNames = mc.getColumnNames();
2887        for (int i = 0; i < columnNames.length; i++) {
2888            builder.put(columnNames[i], i);
2889        }
2890        final Map<String, Integer> colPosMap = builder.build();
2891
2892        final MailPrefs mailPrefs = MailPrefs.get(getContext());
2893
2894        final Object[] values = new Object[columnNames.length];
2895        if (colPosMap.containsKey(BaseColumns._ID)) {
2896            values[colPosMap.get(BaseColumns._ID)] = 0;
2897        }
2898        if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) {
2899            values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] =
2900                    AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE;
2901        }
2902        if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) {
2903            values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] =
2904                    combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING);
2905        }
2906        if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) {
2907            values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString(
2908                R.string.mailbox_list_account_selector_combined_view);
2909        }
2910        if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) {
2911            values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown";
2912        }
2913        if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) {
2914            values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] =
2915                    "'content://" + EmailContent.AUTHORITY + "/uiundo'";
2916        }
2917        if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) {
2918            values[colPosMap.get(UIProvider.AccountColumns.URI)] =
2919                    combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING);
2920        }
2921        if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) {
2922            values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] =
2923                    EMAIL_APP_MIME_TYPE;
2924        }
2925        if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
2926            values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] =
2927                    getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING);
2928        }
2929        if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) {
2930            values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] =
2931                    getExternalUriStringEmail2("compose", Long.toString(id));
2932        }
2933
2934        // TODO: Get these from default account?
2935        Preferences prefs = Preferences.getPreferences(getContext());
2936        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
2937            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] =
2938                    Integer.toString(UIProvider.AutoAdvance.NEWER);
2939        }
2940        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) {
2941            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] =
2942                    Integer.toString(UIProvider.MessageTextSize.NORMAL);
2943        }
2944        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) {
2945            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] =
2946                    Integer.toString(UIProvider.SnapHeaderValue.ALWAYS);
2947        }
2948        //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
2949        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
2950            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] =
2951                    Integer.toString(mailPrefs.getDefaultReplyAll()
2952                            ? UIProvider.DefaultReplyBehavior.REPLY_ALL
2953                            : UIProvider.DefaultReplyBehavior.REPLY);
2954        }
2955        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
2956            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] =
2957                    getConversationListIcon(mailPrefs);
2958        }
2959        if (colPosMap.containsKey(
2960                UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) {
2961            values[colPosMap.get(
2962                    UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)] = 0;
2963        }
2964        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
2965            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] =
2966                    prefs.getConfirmDelete() ? 1 : 0;
2967        }
2968        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) {
2969            values[colPosMap.get(
2970                    UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0;
2971        }
2972        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
2973            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] =
2974                    prefs.getConfirmSend() ? 1 : 0;
2975        }
2976        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) {
2977            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] =
2978                    combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
2979        }
2980        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) {
2981            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] =
2982                    combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
2983        }
2984
2985        mc.addRow(values);
2986    }
2987
2988    private static int getConversationListIcon(MailPrefs mailPrefs) {
2989        return mailPrefs.getShowSenderImages() ?
2990                UIProvider.ConversationListIcon.SENDER_IMAGE :
2991                UIProvider.ConversationListIcon.NONE;
2992    }
2993
2994    private Cursor getVirtualMailboxCursor(long mailboxId) {
2995        MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1);
2996        mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId),
2997                getVirtualMailboxType(mailboxId)));
2998        return mc;
2999    }
3000
3001    private Object[] getVirtualMailboxRow(long accountId, int mailboxType) {
3002        final long id = getVirtualMailboxId(accountId, mailboxType);
3003        final String idString = Long.toString(id);
3004        Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length];
3005        values[UIProvider.FOLDER_ID_COLUMN] = id;
3006        values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString);
3007        values[UIProvider.FOLDER_NAME_COLUMN] = getFolderDisplayName(
3008                getFolderTypeFromMailboxType(mailboxType), "");
3009                // default empty string since all of these should use resource strings
3010        values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0;
3011        values[UIProvider.FOLDER_CAPABILITIES_COLUMN] =
3012                UIProvider.FolderCapabilities.DELETE | UIProvider.FolderCapabilities.IS_VIRTUAL;
3013        values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages",
3014                idString);
3015
3016        // Do any special handling
3017        final String accountIdString = Long.toString(accountId);
3018        switch (mailboxType) {
3019            case Mailbox.TYPE_INBOX:
3020                if (accountId == COMBINED_ACCOUNT_ID) {
3021                    // Add the unread count
3022                    final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3023                            MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID
3024                            + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE
3025                            + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0",
3026                            null);
3027                    values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount;
3028                }
3029                // Add the icon
3030                values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_inbox;
3031                break;
3032            case Mailbox.TYPE_UNREAD: {
3033                // Add the unread count
3034                final String accountKeyClause;
3035                final String[] whereArgs;
3036                if (accountId == COMBINED_ACCOUNT_ID) {
3037                    accountKeyClause = "";
3038                    whereArgs = null;
3039                } else {
3040                    accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
3041                    whereArgs = new String[] { accountIdString };
3042                }
3043                final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3044                        accountKeyClause + MessageColumns.FLAG_READ + "=0 AND "
3045                        + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns.ID
3046                        + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "="
3047                        + Mailbox.TYPE_TRASH + ")", whereArgs);
3048                values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount;
3049                // Add the icon
3050                values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_unread;
3051                break;
3052            } case Mailbox.TYPE_STARRED: {
3053                // Add the starred count as the unread count
3054                final String accountKeyClause;
3055                final String[] whereArgs;
3056                if (accountId == COMBINED_ACCOUNT_ID) {
3057                    accountKeyClause = "";
3058                    whereArgs = null;
3059                } else {
3060                    accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
3061                    whereArgs = new String[] { accountIdString };
3062                }
3063                final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3064                        accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs);
3065                values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = starredCount;
3066                // Add the icon
3067                values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_star;
3068                break;
3069            }
3070        }
3071
3072        return values;
3073    }
3074
3075    private Cursor uiAccounts(String[] uiProjection) {
3076        final Context context = getContext();
3077        final SQLiteDatabase db = getDatabase(context);
3078        final Cursor accountIdCursor =
3079                db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
3080        final MatrixCursor mc;
3081        try {
3082            boolean combinedAccount = false;
3083            if (accountIdCursor.getCount() > 1) {
3084                combinedAccount = true;
3085            }
3086            final Bundle extras = new Bundle();
3087            // Email always returns the accurate number of accounts
3088            extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1);
3089            mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras);
3090            final Object[] values = new Object[uiProjection.length];
3091            while (accountIdCursor.moveToNext()) {
3092                final String id = accountIdCursor.getString(0);
3093                final Cursor accountCursor =
3094                        db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
3095                try {
3096                    if (accountCursor.moveToNext()) {
3097                        for (int i = 0; i < uiProjection.length; i++) {
3098                            values[i] = accountCursor.getString(i);
3099                        }
3100                        mc.addRow(values);
3101                    }
3102                } finally {
3103                    accountCursor.close();
3104                }
3105            }
3106            if (combinedAccount) {
3107                addCombinedAccountRow(mc);
3108            }
3109        } finally {
3110            accountIdCursor.close();
3111        }
3112        mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER);
3113
3114        return mc;
3115    }
3116
3117    private Cursor uiQuickResponseAccount(String[] uiProjection, String account) {
3118        final Context context = getContext();
3119        final SQLiteDatabase db = getDatabase(context);
3120        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
3121        sb.append(" FROM " + QuickResponse.TABLE_NAME);
3122        sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?");
3123        final String query = sb.toString();
3124        return db.rawQuery(query, new String[] {account});
3125    }
3126
3127    private Cursor uiQuickResponseId(String[] uiProjection, String id) {
3128        final Context context = getContext();
3129        final SQLiteDatabase db = getDatabase(context);
3130        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
3131        sb.append(" FROM " + QuickResponse.TABLE_NAME);
3132        sb.append(" WHERE " + QuickResponse.ID + "=?");
3133        final String query = sb.toString();
3134        return db.rawQuery(query, new String[] {id});
3135    }
3136
3137    private Cursor uiQuickResponse(String[] uiProjection) {
3138        final Context context = getContext();
3139        final SQLiteDatabase db = getDatabase(context);
3140        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
3141        sb.append(" FROM " + QuickResponse.TABLE_NAME);
3142        final String query = sb.toString();
3143        return db.rawQuery(query, new String[0]);
3144    }
3145
3146    /**
3147     * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
3148     *
3149     * @param uiProjection as passed from UnifiedEmail
3150     * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments
3151     * or null if there are no query parameters
3152     * @return the SQLite query to be executed on the EmailProvider database
3153     */
3154    private static String genQueryAttachments(String[] uiProjection,
3155            List<String> contentTypeQueryParameters) {
3156        // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT
3157        ContentValues values = new ContentValues(1);
3158        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
3159        StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
3160        sb.append(" FROM ")
3161                .append(Attachment.TABLE_NAME)
3162                .append(" WHERE ")
3163                .append(AttachmentColumns.MESSAGE_KEY)
3164                .append(" =? ");
3165
3166        // Filter for certain content types.
3167        // The filter works by adding LIKE operators for each
3168        // content type you wish to request. Content types
3169        // are filtered by performing a case-insensitive "starts with"
3170        // filter. IE, "image/" would return "image/png" as well as "image/jpeg".
3171        if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
3172            final int size = contentTypeQueryParameters.size();
3173            sb.append("AND (");
3174            for (int i = 0; i < size; i++) {
3175                final String contentType = contentTypeQueryParameters.get(i);
3176                sb.append(AttachmentColumns.MIME_TYPE)
3177                        .append(" LIKE '")
3178                        .append(contentType)
3179                        .append("%'");
3180
3181                if (i != size - 1) {
3182                    sb.append(" OR ");
3183                }
3184            }
3185            sb.append(")");
3186        }
3187        return sb.toString();
3188    }
3189
3190    /**
3191     * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
3192     *
3193     * @param uiProjection as passed from UnifiedEmail
3194     * @return the SQLite query to be executed on the EmailProvider database
3195     */
3196    private String genQueryAttachment(String[] uiProjection, String idString) {
3197        Long id = Long.parseLong(idString);
3198        Attachment att = Attachment.restoreAttachmentWithId(getContext(), id);
3199        // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS
3200        ContentValues values = new ContentValues(2);
3201        values.put(AttachmentColumns.CONTENT_URI,
3202                AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString());
3203        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
3204        StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
3205        sb.append(" FROM ")
3206                .append(Attachment.TABLE_NAME)
3207                .append(" WHERE ")
3208                .append(AttachmentColumns.ID)
3209                .append(" =? ");
3210        return sb.toString();
3211    }
3212
3213    /**
3214     * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
3215     *
3216     * @param uiProjection as passed from UnifiedEmail
3217     * @return the SQLite query to be executed on the EmailProvider database
3218     */
3219    private static String genQuerySubfolders(String[] uiProjection) {
3220        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
3221        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
3222                " =? ORDER BY ");
3223        sb.append(MAILBOX_ORDER_BY);
3224        return sb.toString();
3225    }
3226
3227    private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID);
3228
3229    /**
3230     * Returns a cursor over all the folders for a specific URI which corresponds to a single
3231     * account.
3232     * @param uri uri to query
3233     * @param uiProjection projection
3234     * @return query result cursor
3235     */
3236    private Cursor uiFolders(Uri uri, String[] uiProjection) {
3237        Context context = getContext();
3238        SQLiteDatabase db = getDatabase(context);
3239        String id = uri.getPathSegments().get(1);
3240        if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
3241            MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 3);
3242            Object[] row;
3243            row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX);
3244            mc.addRow(row);
3245            row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED);
3246            mc.addRow(row);
3247            row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD);
3248            mc.addRow(row);
3249
3250            final Uri notifyUri =
3251                    UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
3252            mc.setNotificationUri(context.getContentResolver(), notifyUri);
3253            return mc;
3254        } else {
3255            Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id});
3256            c = getFolderListCursor(db, c, uiProjection);
3257            // Add starred virtual folder to the cursor
3258            // Show number of messages as unread count (for backward compatibility)
3259            MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 2);
3260            final long acctId = Long.parseLong(id);
3261            Object[] row = getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED);
3262            mc.addRow(row);
3263            row = getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD);
3264            mc.addRow(row);
3265
3266            final Uri notifyUri =
3267                    UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
3268            mc.setNotificationUri(context.getContentResolver(), notifyUri);
3269            c.setNotificationUri(context.getContentResolver(), notifyUri);
3270            Cursor[] cursors = new Cursor[] {mc, c};
3271            return new MergeCursor(cursors);
3272        }
3273    }
3274
3275    /**
3276     * Returns an array of the default recent folders for a given URI which is unique for an
3277     * account. Some accounts might not have default recent folders, in which case an empty array
3278     * is returned.
3279     * @param id account id
3280     * @return array of URIs
3281     */
3282    private Uri[] defaultRecentFolders(final String id) {
3283        final SQLiteDatabase db = getDatabase(getContext());
3284        if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
3285            // We don't have default recents for the combined view.
3286            return new Uri[0];
3287        }
3288        // We search for the types we want, and find corresponding IDs.
3289        final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE };
3290
3291        // Sent, Drafts, and Starred are the default recents.
3292        final StringBuilder sb = genSelect(getFolderListMap(), idAndType);
3293        sb.append(" FROM ")
3294                .append(Mailbox.TABLE_NAME)
3295                .append(" WHERE ")
3296                .append(MailboxColumns.ACCOUNT_KEY)
3297                .append(" = ")
3298                .append(id)
3299                .append(" AND ")
3300                .append(MailboxColumns.TYPE)
3301                .append(" IN (")
3302                .append(Mailbox.TYPE_SENT)
3303                .append(", ")
3304                .append(Mailbox.TYPE_DRAFTS)
3305                .append(", ")
3306                .append(Mailbox.TYPE_STARRED)
3307                .append(")");
3308        LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb);
3309        final Cursor c = db.rawQuery(sb.toString(), null);
3310        if (c == null || c.getCount() <= 0 || !c.moveToFirst()) {
3311            return new Uri[0];
3312        }
3313        // Read all the IDs of the mailboxes, and turn them into URIs.
3314        final Uri[] recentFolders = new Uri[c.getCount()];
3315        int i = 0;
3316        do {
3317            final long folderId = c.getLong(0);
3318            recentFolders[i] = uiUri("uifolder", folderId);
3319            LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]);
3320            ++i;
3321        } while (c.moveToNext());
3322        return recentFolders;
3323    }
3324
3325    /**
3326     * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so
3327     * any pending notifications for the corresponding mailbox should be canceled). We also handle
3328     * getExtras() to provide a snapshot of the mailbox's status
3329     */
3330    static class EmailConversationCursor extends CursorWrapper {
3331        private final long mMailboxId;
3332        private final Context mContext;
3333        private final FolderList mFolderList;
3334        private final Bundle mExtras = new Bundle();
3335
3336        public EmailConversationCursor(final Context context, final Cursor cursor,
3337                final Folder folder, final long mailboxId) {
3338            super(cursor);
3339            mMailboxId = mailboxId;
3340            mContext = context;
3341            mFolderList = FolderList.copyOf(Lists.newArrayList(folder));
3342            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
3343
3344            // We assume that all message lists are complete
3345            // since we don't do any live lists in email.
3346            mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
3347                    UIProvider.CursorStatus.COMPLETE);
3348            if (mailbox != null) {
3349                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR,
3350                        mailbox.mUiLastSyncResult);
3351                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount);
3352            } else {
3353                // TODO for virtual mailboxes, we may want to do something besides just fake it
3354                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR,
3355                        UIProvider.LastSyncResult.SUCCESS);
3356                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT,
3357                        cursor != null ? cursor.getCount() : 0);
3358            }
3359        }
3360
3361        @Override
3362        public Bundle getExtras() {
3363            return mExtras;
3364        }
3365
3366        /**
3367         * When showing a folder, if it's been at least this long since the last sync,
3368         * force a folder refresh.
3369         */
3370        private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS;
3371
3372        @Override
3373        public Bundle respond(Bundle params) {
3374            final String setVisibilityKey =
3375                    UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY;
3376            if (params.containsKey(setVisibilityKey)) {
3377                final boolean visible = params.getBoolean(setVisibilityKey);
3378                if (visible) {
3379                    // Mark all messages as seen
3380                    final ContentResolver resolver = mContext.getContentResolver();
3381                    final ContentValues contentValues = new ContentValues(1);
3382                    contentValues.put(MessageColumns.FLAG_SEEN, true);
3383                    final Uri uri = EmailContent.Message.CONTENT_URI;
3384                    resolver.update(uri, contentValues, MessageColumns.MAILBOX_KEY + " = ?",
3385                            new String[] {String.valueOf(mMailboxId)});
3386                    if (params.containsKey(
3387                            UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) {
3388                        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
3389                        if (mailbox != null) {
3390                            // For non-push mailboxes, if it's stale (i.e. last sync was a while
3391                            // ago), force a sync.
3392                            // TODO: Fix the check for whether we're non-push? Right now it checks
3393                            // whether we are participating in account sync rules.
3394                            if (mailbox.mSyncInterval == 0) {
3395                                final long timeSinceLastSync =
3396                                        System.currentTimeMillis() - mailbox.mSyncTime;
3397                                if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) {
3398                                    final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI +
3399                                            "/" + QUERY_UIREFRESH + "/" + mailbox.mId);
3400                                    resolver.query(refreshUri, null, null, null, null);
3401                                }
3402                            }
3403                        }
3404                    }
3405                }
3406            }
3407            // Return success
3408            final Bundle response = new Bundle(2);
3409
3410            response.putString(setVisibilityKey,
3411                    UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK);
3412
3413            final String rawFoldersKey =
3414                    UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS;
3415            if (params.containsKey(rawFoldersKey)) {
3416                response.putParcelable(rawFoldersKey, mFolderList);
3417            }
3418
3419            return response;
3420        }
3421    }
3422
3423    /**
3424     * Convenience method to create a {@link Folder}
3425     * @param context to get a {@link ContentResolver}
3426     * @param mailboxId id of the {@link Mailbox} that we want
3427     * @return the {@link Folder} or null
3428     */
3429    public static Folder getFolder(Context context, long mailboxId) {
3430        final ContentResolver resolver = context.getContentResolver();
3431        final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId),
3432                UIProvider.FOLDERS_PROJECTION, null, null, null);
3433
3434        if (fc == null) {
3435            LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId);
3436            return null;
3437        }
3438
3439        Folder uiFolder = null;
3440        try {
3441            if (fc.moveToFirst()) {
3442                uiFolder = new Folder(fc);
3443            }
3444        } finally {
3445            fc.close();
3446        }
3447        return uiFolder;
3448    }
3449
3450    static class AttachmentsCursor extends CursorWrapper {
3451        private final int mContentUriIndex;
3452        private final int mUriIndex;
3453        private final Context mContext;
3454
3455        public AttachmentsCursor(Context context, Cursor cursor) {
3456            super(cursor);
3457            mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI);
3458            mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI);
3459            mContext = context;
3460        }
3461
3462        @Override
3463        public String getString(int column) {
3464            if (column == mContentUriIndex) {
3465                final Uri uri = Uri.parse(getString(mUriIndex));
3466                final long id = Long.parseLong(uri.getLastPathSegment());
3467                final Attachment att = Attachment.restoreAttachmentWithId(mContext, id);
3468                if (att == null) return "";
3469
3470                final String contentUri;
3471                // Until the package installer can handle opening apks from a content:// uri, for
3472                // any apk that was successfully saved in external storage, return the
3473                // content uri from the attachment
3474                if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL &&
3475                        att.mUiState == UIProvider.AttachmentState.SAVED &&
3476                        TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) {
3477                    contentUri = att.getContentUri();
3478                } else {
3479                    contentUri =
3480                            AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString();
3481                }
3482                return contentUri;
3483            } else {
3484                return super.getString(column);
3485            }
3486        }
3487    }
3488
3489    /**
3490     * For debugging purposes; shouldn't be used in production code
3491     */
3492    @SuppressWarnings("unused")
3493    static class CloseDetectingCursor extends CursorWrapper {
3494
3495        public CloseDetectingCursor(Cursor cursor) {
3496            super(cursor);
3497        }
3498
3499        @Override
3500        public void close() {
3501            super.close();
3502            LogUtils.d(TAG, "Closing cursor", new Error());
3503        }
3504    }
3505
3506    /**
3507     * We need to do individual queries for the mailboxes in order to get correct
3508     * folder capabilities.
3509     */
3510    private Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) {
3511        final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
3512        final String[] args = new String[1];
3513        final int projectionLength = uiProjection.length;
3514        final List<String> projectionList = Arrays.asList(uiProjection);
3515        final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME);
3516        final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE);
3517
3518        try {
3519            // Loop through mailboxes, building matrix cursor
3520            while (c.moveToNext()) {
3521                final String id = c.getString(0);
3522                args[0] = id;
3523                final Cursor mailboxCursor = db.rawQuery(genQueryMailbox(uiProjection, id), args);
3524                if (mailboxCursor.moveToNext()) {
3525                    getUiFolderCursorRowFromMailboxCursorRow(
3526                            mc, projectionLength, mailboxCursor, nameColumn, typeColumn);
3527                }
3528            }
3529        } finally {
3530            c.close();
3531        }
3532       return mc;
3533    }
3534
3535    /**
3536     * Converts a mailbox in a row of the mailboxCursor into a row
3537     * in the supplied {@link MatrixCursor} in the format required for {@link Folder}.
3538     * As a convenience, the modified {@link MatrixCursor} is also returned.
3539     * @param mc the {@link MatrixCursor} into which the mailbox data will be converted
3540     * @param projectionLength the length of the projection for this Cursor
3541     * @param mailboxCursor the cursor supplying the mailbox data
3542     * @param nameColumn column in the cursor containing the folder name value
3543     * @param typeColumn column in the cursor containing the folder type value
3544     * @return the {@link MatrixCursor} containing the transformed data.
3545     */
3546    private Cursor getUiFolderCursorRowFromMailboxCursorRow(
3547            MatrixCursor mc, int projectionLength, Cursor mailboxCursor,
3548            int nameColumn, int typeColumn) {
3549        final MatrixCursor.RowBuilder builder = mc.newRow();
3550        for (int i = 0; i < projectionLength; i++) {
3551            // If we are at the name column, get the type
3552            // and use it to use a properly translated string
3553            // from resources instead of the display name.
3554            // This ignores display names for system mailboxes.
3555            if (nameColumn == i) {
3556                // We implicitly assume that if name is requested,
3557                // type has also been requested. If not, this will
3558                // error in unknown ways.
3559                final int type = mailboxCursor.getInt(typeColumn);
3560                builder.add(getFolderDisplayName(type, mailboxCursor.getString(i)));
3561            } else {
3562                builder.add(mailboxCursor.getString(i));
3563            }
3564        }
3565        return mc;
3566    }
3567
3568    /**
3569     * Returns a {@link String} from Resources corresponding
3570     * to the {@link UIProvider.FolderType} requested.
3571     * @param folderType {@link UIProvider.FolderType} value for the folder
3572     * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType}
3573     *                    provided is not a system folder.
3574     * @return a {@link String} to use as the display name for the folder
3575     */
3576    private String getFolderDisplayName(int folderType, String defaultName) {
3577        final int resId;
3578        switch (folderType) {
3579            case UIProvider.FolderType.INBOX:
3580                resId = R.string.mailbox_name_display_inbox;
3581                break;
3582            case UIProvider.FolderType.OUTBOX:
3583                resId = R.string.mailbox_name_display_outbox;
3584                break;
3585            case UIProvider.FolderType.DRAFT:
3586                resId = R.string.mailbox_name_display_drafts;
3587                break;
3588            case UIProvider.FolderType.TRASH:
3589                resId = R.string.mailbox_name_display_trash;
3590                break;
3591            case UIProvider.FolderType.SENT:
3592                resId = R.string.mailbox_name_display_sent;
3593                break;
3594            case UIProvider.FolderType.SPAM:
3595                resId = R.string.mailbox_name_display_junk;
3596                break;
3597            case UIProvider.FolderType.STARRED:
3598                resId = R.string.mailbox_name_display_starred;
3599                break;
3600            case UIProvider.FolderType.UNREAD:
3601                resId = R.string.mailbox_name_display_unread;
3602                break;
3603            default:
3604                return defaultName;
3605        }
3606        return getContext().getString(resId);
3607    }
3608
3609    /**
3610     * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType}
3611     * equivalent.
3612     * @param mailboxType a {@link Mailbox} type
3613     * @return a {@link UIProvider.FolderType} value
3614     */
3615    private static int getFolderTypeFromMailboxType(int mailboxType) {
3616        switch (mailboxType) {
3617            case Mailbox.TYPE_INBOX:
3618                return UIProvider.FolderType.INBOX;
3619            case Mailbox.TYPE_OUTBOX:
3620                return UIProvider.FolderType.OUTBOX;
3621            case Mailbox.TYPE_DRAFTS:
3622                return UIProvider.FolderType.DRAFT;
3623            case Mailbox.TYPE_TRASH:
3624                return UIProvider.FolderType.TRASH;
3625            case Mailbox.TYPE_SENT:
3626                return UIProvider.FolderType.SENT;
3627            case Mailbox.TYPE_JUNK:
3628                return UIProvider.FolderType.SPAM;
3629            case Mailbox.TYPE_STARRED:
3630                return UIProvider.FolderType.STARRED;
3631            case Mailbox.TYPE_UNREAD:
3632                return UIProvider.FolderType.UNREAD;
3633            case Mailbox.TYPE_SEARCH:
3634                // TODO Can the DEFAULT type be removed from SEARCH folders?
3635                return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH;
3636            default:
3637                return UIProvider.FolderType.DEFAULT;
3638        }
3639    }
3640
3641    /**
3642     * Handle UnifiedEmail queries here (dispatched from query())
3643     *
3644     * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
3645     * @param uri the original uri passed in from UnifiedEmail
3646     * @param uiProjection the projection passed in from UnifiedEmail
3647     * @param unseenOnly <code>true</code> to only return unseen messages (where supported)
3648     * @return the result Cursor
3649     */
3650    private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) {
3651        Context context = getContext();
3652        ContentResolver resolver = context.getContentResolver();
3653        SQLiteDatabase db = getDatabase(context);
3654        // Should we ever return null, or throw an exception??
3655        Cursor c = null;
3656        String id = uri.getPathSegments().get(1);
3657        Uri notifyUri = null;
3658        switch(match) {
3659            case UI_ALL_FOLDERS:
3660                // TODO: Should this just do uiFolders()?
3661                c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id});
3662                c = getFolderListCursor(db, c, uiProjection);
3663                notifyUri =
3664                        UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
3665                break;
3666            case UI_RECENT_FOLDERS:
3667                c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id});
3668                notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
3669                break;
3670            case UI_SUBFOLDERS:
3671                c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id});
3672                c = getFolderListCursor(db, c, uiProjection);
3673                // Get notifications for any folder changes on this account. This is broader than
3674                // we need but otherwise we'd need for every folder change to notify on all relevant
3675                // subtrees. For now we opt for simplicity.
3676                final long accountId = Mailbox.getAccountIdForMailbox(context, id);
3677                notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
3678                break;
3679            case UI_MESSAGES:
3680                long mailboxId = Long.parseLong(id);
3681                final Folder folder = getFolder(context, mailboxId);
3682                if (folder == null) {
3683                    // This mailboxId is bogus.
3684                    return null;
3685                }
3686                if (isVirtualMailbox(mailboxId)) {
3687                    c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly);
3688                } else {
3689                    c = db.rawQuery(
3690                            genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id});
3691                }
3692                notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
3693                c = new EmailConversationCursor(context, c, folder, mailboxId);
3694                break;
3695            case UI_MESSAGE:
3696                MessageQuery qq = genQueryViewMessage(uiProjection, id);
3697                String sql = qq.query;
3698                String attJson = qq.attachmentJson;
3699                // With attachments, we have another argument to bind
3700                if (attJson != null) {
3701                    c = db.rawQuery(sql, new String[] {attJson, id});
3702                } else {
3703                    c = db.rawQuery(sql, new String[] {id});
3704                }
3705                break;
3706            case UI_ATTACHMENTS:
3707                final List<String> contentTypeQueryParameters =
3708                        uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
3709                c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters),
3710                        new String[] {id});
3711                c = new AttachmentsCursor(context, c);
3712                notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
3713                break;
3714            case UI_ATTACHMENT:
3715                c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id});
3716                notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
3717                break;
3718            case UI_FOLDER:
3719                mailboxId = Long.parseLong(id);
3720                if (isVirtualMailbox(mailboxId)) {
3721                    c = getVirtualMailboxCursor(mailboxId);
3722                    notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build();
3723                } else {
3724                    c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[]{id});
3725                    final List<String> projectionList = Arrays.asList(uiProjection);
3726                    final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME);
3727                    final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE);
3728                    if (c.moveToFirst()) {
3729                        c = getUiFolderCursorRowFromMailboxCursorRow(
3730                                new MatrixCursorWithCachedColumns(uiProjection),
3731                                uiProjection.length, c, nameColumn, typeColumn);
3732                    }
3733                    notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build();
3734                }
3735                break;
3736            case UI_ACCOUNT:
3737                if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
3738                    MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1);
3739                    addCombinedAccountRow(mc);
3740                    c = mc;
3741                } else {
3742                    c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
3743                }
3744                notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
3745                break;
3746            case UI_CONVERSATION:
3747                c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id});
3748                break;
3749        }
3750        if (notifyUri != null) {
3751            c.setNotificationUri(resolver, notifyUri);
3752        }
3753        return c;
3754    }
3755
3756    /**
3757     * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need
3758     * a few of the fields
3759     * @param uiAtt the UIProvider attachment to convert
3760     * @param cachedFile the path to the cached file to
3761     * @return the EmailProvider attachment
3762     */
3763    // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be
3764    // removed
3765    private static Attachment convertUiAttachmentToAttachment(
3766            com.android.mail.providers.Attachment uiAtt, String cachedFile) {
3767        final Attachment att = new Attachment();
3768        att.setContentUri(uiAtt.contentUri.toString());
3769
3770        if (!TextUtils.isEmpty(cachedFile)) {
3771            // Generate the content provider uri for this cached file
3772            final Uri.Builder cachedFileBuilder = Uri.parse(
3773                    "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon();
3774            cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile);
3775            att.setCachedFileUri(cachedFileBuilder.build().toString());
3776        }
3777
3778        att.mFileName = uiAtt.getName();
3779        att.mMimeType = uiAtt.getContentType();
3780        att.mSize = uiAtt.size;
3781        return att;
3782    }
3783
3784    /**
3785     * Create a mailbox given the account and mailboxType.
3786     */
3787    private Mailbox createMailbox(long accountId, int mailboxType) {
3788        Context context = getContext();
3789        Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType);
3790        // Make sure drafts and save will show up in recents...
3791        // If these already exist (from old Email app), they will have touch times
3792        switch (mailboxType) {
3793            case Mailbox.TYPE_DRAFTS:
3794                box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
3795                break;
3796            case Mailbox.TYPE_SENT:
3797                box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
3798                break;
3799        }
3800        box.save(context);
3801        return box;
3802    }
3803
3804    /**
3805     * Given an account name and a mailbox type, return that mailbox, creating it if necessary
3806     * @param accountId the account id to use
3807     * @param mailboxType the type of mailbox we're trying to find
3808     * @return the mailbox of the given type for the account in the uri, or null if not found
3809     */
3810    private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) {
3811        Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType);
3812        if (mailbox == null) {
3813            mailbox = createMailbox(accountId, mailboxType);
3814        }
3815        return mailbox;
3816    }
3817
3818    /**
3819     * Given a mailbox and the content values for a message, create/save the message in the mailbox
3820     * @param mailbox the mailbox to use
3821     * @param extras the bundle containing the message fields
3822     * @return the uri of the newly created message
3823     * TODO(yph): The following fields are available in extras but unused, verify whether they
3824     *     should be respected:
3825     *     - UIProvider.MessageColumns.SNIPPET
3826     *     - UIProvider.MessageColumns.REPLY_TO
3827     *     - UIProvider.MessageColumns.FROM
3828     *     - UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS
3829     */
3830    private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) {
3831        final Context context = getContext();
3832        // Fill in the message
3833        final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
3834        if (account == null) return null;
3835        msg.mFrom = account.mEmailAddress;
3836        msg.mTimeStamp = System.currentTimeMillis();
3837        msg.mTo = extras.getString(UIProvider.MessageColumns.TO);
3838        msg.mCc = extras.getString(UIProvider.MessageColumns.CC);
3839        msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC);
3840        msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT);
3841        msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT);
3842        msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML);
3843        msg.mMailboxKey = mailbox.mId;
3844        msg.mAccountKey = mailbox.mAccountKey;
3845        msg.mDisplayName = msg.mTo;
3846        msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
3847        msg.mFlagRead = true;
3848        msg.mFlagSeen = true;
3849        final Integer quoteStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
3850        msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos;
3851        int flags = 0;
3852        final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE);
3853        switch(draftType) {
3854            case DraftType.FORWARD:
3855                flags |= Message.FLAG_TYPE_FORWARD;
3856                break;
3857            case DraftType.REPLY_ALL:
3858                flags |= Message.FLAG_TYPE_REPLY_ALL;
3859                //$FALL-THROUGH$
3860            case DraftType.REPLY:
3861                flags |= Message.FLAG_TYPE_REPLY;
3862                break;
3863            case DraftType.COMPOSE:
3864                flags |= Message.FLAG_TYPE_ORIGINAL;
3865                break;
3866        }
3867        int draftInfo = 0;
3868        if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) {
3869            draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
3870            if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) {
3871                draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE;
3872            }
3873        }
3874        if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) {
3875            flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT;
3876        }
3877        msg.mDraftInfo = draftInfo;
3878        msg.mFlags = flags;
3879
3880        final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID);
3881        if (ref != null && msg.mQuotedTextStartPos >= 0) {
3882            String refId = Uri.parse(ref).getLastPathSegment();
3883            try {
3884                msg.mSourceKey = Long.parseLong(refId);
3885            } catch (NumberFormatException e) {
3886                // This will be zero; the default
3887            }
3888        }
3889
3890        // Get attachments from the ContentValues
3891        final List<com.android.mail.providers.Attachment> uiAtts =
3892                com.android.mail.providers.Attachment.fromJSONArray(
3893                        extras.getString(UIProvider.MessageColumns.ATTACHMENTS));
3894        final ArrayList<Attachment> atts = new ArrayList<Attachment>();
3895        boolean hasUnloadedAttachments = false;
3896        Bundle attachmentFds =
3897                extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP);
3898        for (com.android.mail.providers.Attachment uiAtt: uiAtts) {
3899            final Uri attUri = uiAtt.uri;
3900            if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) {
3901                // If it's one of ours, retrieve the attachment and add it to the list
3902                final long attId = Long.parseLong(attUri.getLastPathSegment());
3903                final Attachment att = Attachment.restoreAttachmentWithId(context, attId);
3904                if (att != null) {
3905                    // We must clone the attachment into a new one for this message; easiest to
3906                    // use a parcel here
3907                    final Parcel p = Parcel.obtain();
3908                    att.writeToParcel(p, 0);
3909                    p.setDataPosition(0);
3910                    final Attachment attClone = new Attachment(p);
3911                    p.recycle();
3912                    // Clear the messageKey (this is going to be a new attachment)
3913                    attClone.mMessageKey = 0;
3914                    // If we're sending this, it's not loaded, and we're not smart forwarding
3915                    // add the download flag, so that ADS will start up
3916                    if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null &&
3917                            ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
3918                        attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD;
3919                        hasUnloadedAttachments = true;
3920                    }
3921                    atts.add(attClone);
3922                }
3923            } else {
3924                // Cache the attachment.  This will allow us to send it, if the permissions are
3925                // revoked
3926                final String cachedFileUri =
3927                        AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds);
3928
3929                // Convert external attachment to one of ours and add to the list
3930                atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri));
3931            }
3932        }
3933        if (!atts.isEmpty()) {
3934            msg.mAttachments = atts;
3935            msg.mFlagAttachment = true;
3936            if (hasUnloadedAttachments) {
3937                Utility.showToast(context, R.string.message_view_attachment_background_load);
3938            }
3939        }
3940        // Save it or update it...
3941        if (!msg.isSaved()) {
3942            msg.save(context);
3943        } else {
3944            // This is tricky due to how messages/attachments are saved; rather than putz with
3945            // what's changed, we'll delete/re-add them
3946            final ArrayList<ContentProviderOperation> ops =
3947                    new ArrayList<ContentProviderOperation>();
3948            // Delete all existing attachments
3949            ops.add(ContentProviderOperation.newDelete(
3950                    ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId))
3951                    .build());
3952            // Delete the body
3953            ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI)
3954                    .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)})
3955                    .build());
3956            // Add the ops for the message, atts, and body
3957            msg.addSaveOps(ops);
3958            // Do it!
3959            try {
3960                applyBatch(ops);
3961            } catch (OperationApplicationException e) {
3962                LogUtils.d(TAG, "applyBatch exception");
3963            }
3964        }
3965        if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
3966            startSync(mailbox, 0);
3967            final long originalMsgId = msg.mSourceKey;
3968            if (originalMsgId != 0) {
3969                final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId);
3970                // If the original message exists, set its forwarded/replied to flags
3971                if (originalMsg != null) {
3972                    final ContentValues cv = new ContentValues();
3973                    flags = originalMsg.mFlags;
3974                    switch(draftType) {
3975                        case DraftType.FORWARD:
3976                            flags |= Message.FLAG_FORWARDED;
3977                            break;
3978                        case DraftType.REPLY_ALL:
3979                        case DraftType.REPLY:
3980                            flags |= Message.FLAG_REPLIED_TO;
3981                            break;
3982                    }
3983                    cv.put(Message.FLAGS, flags);
3984                    context.getContentResolver().update(ContentUris.withAppendedId(
3985                            Message.CONTENT_URI, originalMsgId), cv, null, null);
3986                }
3987            }
3988        }
3989        return uiUri("uimessage", msg.mId);
3990    }
3991
3992    private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) {
3993        final Mailbox mailbox =
3994                getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS);
3995        if (mailbox == null) return null;
3996        final Message msg;
3997        if (extras.containsKey(BaseColumns._ID)) {
3998            final long messageId = extras.getLong(BaseColumns._ID);
3999            msg = Message.restoreMessageWithId(getContext(), messageId);
4000        } else {
4001            msg = new Message();
4002        }
4003        return uiSaveMessage(msg, mailbox, extras);
4004    }
4005
4006    private Uri uiSendDraftMessage(final long accountId, final Bundle extras) {
4007        final Context context = getContext();
4008        final Message msg;
4009        if (extras.containsKey(BaseColumns._ID)) {
4010            final long messageId = extras.getLong(BaseColumns._ID);
4011            msg = Message.restoreMessageWithId(getContext(), messageId);
4012        } else {
4013            msg = new Message();
4014        }
4015
4016        if (msg == null) return null;
4017        final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX);
4018        if (mailbox == null) return null;
4019        // Make sure the sent mailbox exists, since it will be necessary soon.
4020        // TODO(yph): move system mailbox creation to somewhere sane.
4021        final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT);
4022        if (sentMailbox == null) return null;
4023        final Uri messageUri = uiSaveMessage(msg, mailbox, extras);
4024        // Kick observers
4025        context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null);
4026        return messageUri;
4027    }
4028
4029    private static void putIntegerLongOrBoolean(ContentValues values, String columnName,
4030            Object value) {
4031        if (value instanceof Integer) {
4032            Integer intValue = (Integer)value;
4033            values.put(columnName, intValue);
4034        } else if (value instanceof Boolean) {
4035            Boolean boolValue = (Boolean)value;
4036            values.put(columnName, boolValue ? 1 : 0);
4037        } else if (value instanceof Long) {
4038            Long longValue = (Long)value;
4039            values.put(columnName, longValue);
4040        }
4041    }
4042
4043    /**
4044     * Update the timestamps for the folders specified and notifies on the recent folder URI.
4045     * @param folders array of folder Uris to update
4046     * @return number of folders updated
4047     */
4048    private static int updateTimestamp(final Context context, String id, Uri[] folders){
4049        int updated = 0;
4050        final long now = System.currentTimeMillis();
4051        final ContentResolver resolver = context.getContentResolver();
4052        final ContentValues touchValues = new ContentValues();
4053        for (final Uri folder : folders) {
4054            touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now);
4055            LogUtils.d(TAG, "updateStamp: %s updated", folder);
4056            updated += resolver.update(folder, touchValues, null, null);
4057        }
4058        final Uri toNotify =
4059                UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
4060        LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify);
4061        resolver.notifyChange(toNotify, null);
4062        return updated;
4063    }
4064
4065    /**
4066     * Updates the recent folders. The values to be updated are specified as ContentValues pairs
4067     * of (Folder URI, access timestamp). Returns nonzero if successful, always.
4068     * @param uri provider query uri
4069     * @param values uri, timestamp pairs
4070     * @return nonzero value always.
4071     */
4072    private int uiUpdateRecentFolders(Uri uri, ContentValues values) {
4073        final int numFolders = values.size();
4074        final String id = uri.getPathSegments().get(1);
4075        final Uri[] folders = new Uri[numFolders];
4076        final Context context = getContext();
4077        int i = 0;
4078        for (final String uriString : values.keySet()) {
4079            folders[i] = Uri.parse(uriString);
4080        }
4081        return updateTimestamp(context, id, folders);
4082    }
4083
4084    /**
4085     * Populates the recent folders according to the design.
4086     * @param uri provider query uri
4087     * @return the number of recent folders were populated.
4088     */
4089    private int uiPopulateRecentFolders(Uri uri) {
4090        final Context context = getContext();
4091        final String id = uri.getLastPathSegment();
4092        final Uri[] recentFolders = defaultRecentFolders(id);
4093        final int numFolders = recentFolders.length;
4094        if (numFolders <= 0) {
4095            return 0;
4096        }
4097        final int rowsUpdated = updateTimestamp(context, id, recentFolders);
4098        LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated);
4099        return rowsUpdated;
4100    }
4101
4102    private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
4103        int result = 0;
4104        Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
4105        if (stateValue != null) {
4106            // This is a command from UIProvider
4107            long attachmentId = Long.parseLong(uri.getLastPathSegment());
4108            Context context = getContext();
4109            Attachment attachment =
4110                    Attachment.restoreAttachmentWithId(context, attachmentId);
4111            if (attachment == null) {
4112                // Went away; ah, well...
4113                return result;
4114            }
4115            int state = stateValue;
4116            ContentValues values = new ContentValues();
4117            if (state == UIProvider.AttachmentState.NOT_SAVED
4118                    || state == UIProvider.AttachmentState.REDOWNLOADING) {
4119                // Set state, try to cancel request
4120                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED);
4121                values.put(AttachmentColumns.FLAGS,
4122                        attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
4123                attachment.update(context, values);
4124                result = 1;
4125            }
4126            if (state == UIProvider.AttachmentState.DOWNLOADING
4127                    || state == UIProvider.AttachmentState.REDOWNLOADING) {
4128                // Set state and destination; request download
4129                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING);
4130                Integer destinationValue =
4131                        uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
4132                values.put(AttachmentColumns.UI_DESTINATION,
4133                        destinationValue == null ? 0 : destinationValue);
4134                values.put(AttachmentColumns.FLAGS,
4135                        attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
4136                attachment.update(context, values);
4137                result = 1;
4138            }
4139            if (state == UIProvider.AttachmentState.SAVED) {
4140                // If this is an inline attachment, notify message has changed
4141                if (!TextUtils.isEmpty(attachment.mContentId)) {
4142                    notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey);
4143                }
4144                result = 1;
4145            }
4146        }
4147        return result;
4148    }
4149
4150    private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) {
4151        // We need to mark seen separately
4152        if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) {
4153            final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN);
4154
4155            if (seenValue == 1) {
4156                final String mailboxId = uri.getLastPathSegment();
4157                final int rows = markAllSeen(context, mailboxId);
4158
4159                if (uiValues.size() == 1) {
4160                    // Nothing else to do, so return this value
4161                    return rows;
4162                }
4163            }
4164        }
4165
4166        final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true);
4167        if (ourUri == null) return 0;
4168        ContentValues ourValues = new ContentValues();
4169        // This should only be called via update to "recent folders"
4170        for (String columnName: uiValues.keySet()) {
4171            if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) {
4172                ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName));
4173            }
4174        }
4175        return update(ourUri, ourValues, null, null);
4176    }
4177
4178    private int markAllSeen(final Context context, final String mailboxId) {
4179        final SQLiteDatabase db = getDatabase(context);
4180        final String table = Message.TABLE_NAME;
4181        final ContentValues values = new ContentValues(1);
4182        values.put(MessageColumns.FLAG_SEEN, 1);
4183        final String whereClause = MessageColumns.MAILBOX_KEY + " = ?";
4184        final String[] whereArgs = new String[] {mailboxId};
4185
4186        return db.update(table, values, whereClause, whereArgs);
4187    }
4188
4189    private ContentValues convertUiMessageValues(Message message, ContentValues values) {
4190        final ContentValues ourValues = new ContentValues();
4191        for (String columnName : values.keySet()) {
4192            final Object val = values.get(columnName);
4193            if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
4194                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
4195            } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
4196                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
4197            } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) {
4198                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val);
4199            } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
4200                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
4201            } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) {
4202                // Skip this column, as the folders will also be specified  the RAW_FOLDERS column
4203            } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) {
4204                // Convert from folder list uri to mailbox key
4205                final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName));
4206                if (flist.folders.size() != 1) {
4207                    LogUtils.e(TAG,
4208                            "Incorrect number of folders for this message: Message is %s",
4209                            message.mId);
4210                } else {
4211                    final Folder f = flist.folders.get(0);
4212                    final Uri uri = f.folderUri.fullUri;
4213                    final Long mailboxId = Long.parseLong(uri.getLastPathSegment());
4214                    putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
4215                }
4216            } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) {
4217                Address[] fromList = Address.unpack(message.mFrom);
4218                final MailPrefs mailPrefs = MailPrefs.get(getContext());
4219                for (Address sender : fromList) {
4220                    final String email = sender.getAddress();
4221                    mailPrefs.setDisplayImagesFromSender(email, null);
4222                }
4223            } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) ||
4224                    columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) {
4225                // Ignore for now
4226            } else {
4227                throw new IllegalArgumentException("Can't update " + columnName + " in message");
4228            }
4229        }
4230        return ourValues;
4231    }
4232
4233    private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) {
4234        final String idString = uri.getLastPathSegment();
4235        try {
4236            final long id = Long.parseLong(idString);
4237            Uri ourUri = ContentUris.withAppendedId(newBaseUri, id);
4238            if (asProvider) {
4239                ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
4240            }
4241            return ourUri;
4242        } catch (NumberFormatException e) {
4243            return null;
4244        }
4245    }
4246
4247    private Message getMessageFromLastSegment(Uri uri) {
4248        long messageId = Long.parseLong(uri.getLastPathSegment());
4249        return Message.restoreMessageWithId(getContext(), messageId);
4250    }
4251
4252    /**
4253     * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
4254     * clear out the undo list and start over
4255     * @param uri the uri we're working on
4256     * @param op the ContentProviderOperation to perform upon undo
4257     */
4258    private void addToSequence(Uri uri, ContentProviderOperation op) {
4259        String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
4260        if (sequenceString != null) {
4261            int sequence = Integer.parseInt(sequenceString);
4262            if (sequence > mLastSequence) {
4263                // Reset sequence
4264                mLastSequenceOps.clear();
4265                mLastSequence = sequence;
4266            }
4267            // TODO: Need something to indicate a change isn't ready (undoable)
4268            mLastSequenceOps.add(op);
4269        }
4270    }
4271
4272    // TODO: This should depend on flags on the mailbox...
4273    private static boolean uploadsToServer(Context context, Mailbox m) {
4274        if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
4275                m.mType == Mailbox.TYPE_SEARCH) {
4276            return false;
4277        }
4278        String protocol = Account.getProtocol(context, m.mAccountKey);
4279        EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
4280        return (info != null && info.syncChanges);
4281    }
4282
4283    private int uiUpdateMessage(Uri uri, ContentValues values) {
4284        return uiUpdateMessage(uri, values, false);
4285    }
4286
4287    private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) {
4288        Context context = getContext();
4289        Message msg = getMessageFromLastSegment(uri);
4290        if (msg == null) return 0;
4291        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
4292        if (mailbox == null) return 0;
4293        Uri ourBaseUri =
4294                (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI :
4295                    Message.CONTENT_URI;
4296        Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true);
4297        if (ourUri == null) return 0;
4298
4299        // Special case - meeting response
4300        if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) {
4301            final EmailServiceProxy service =
4302                    EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey);
4303            try {
4304                service.sendMeetingResponse(msg.mId,
4305                        values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN));
4306                // Delete the message immediately
4307                uiDeleteMessage(uri);
4308                Utility.showToast(context, R.string.confirm_response);
4309                // Notify box has changed so the deletion is reflected in the UI
4310                notifyUIConversationMailbox(mailbox.mId);
4311            } catch (RemoteException e) {
4312                LogUtils.d(TAG, "Remote exception while sending meeting response");
4313            }
4314            return 1;
4315        }
4316
4317        // Another special case - deleting a draft.
4318        final String operation = values.getAsString(
4319                UIProvider.ConversationOperations.OPERATION_KEY);
4320        if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation)) {
4321            uiDeleteMessage(uri);
4322            return 1;
4323        }
4324
4325        ContentValues undoValues = new ContentValues();
4326        ContentValues ourValues = convertUiMessageValues(msg, values);
4327        for (String columnName: ourValues.keySet()) {
4328            if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
4329                undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
4330            } else if (columnName.equals(MessageColumns.FLAG_READ)) {
4331                undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
4332            } else if (columnName.equals(MessageColumns.FLAG_SEEN)) {
4333                undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen);
4334            } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
4335                undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
4336            }
4337        }
4338        if (undoValues.size() == 0) {
4339            return -1;
4340        }
4341        final Boolean suppressUndo =
4342                values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO);
4343        if (suppressUndo == null || !suppressUndo) {
4344            final ContentProviderOperation op =
4345                    ContentProviderOperation.newUpdate(convertToEmailProviderUri(
4346                            uri, ourBaseUri, false))
4347                            .withValues(undoValues)
4348                            .build();
4349            addToSequence(uri, op);
4350        }
4351
4352        return update(ourUri, ourValues, null, null);
4353    }
4354
4355    /**
4356     * Projection for use with getting mailbox & account keys for a message.
4357     */
4358    private static final String[] MESSAGE_KEYS_PROJECTION =
4359            { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY };
4360    private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0;
4361    private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1;
4362
4363    /**
4364     * Notify necessary UI components in response to a message update.
4365     * @param uri The {@link Uri} for this message update.
4366     * @param messageId The id of the message that's been updated.
4367     * @param values The {@link ContentValues} that were updated in the message.
4368     */
4369    private void handleMessageUpdateNotifications(final Uri uri, final String messageId,
4370            final ContentValues values) {
4371        if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
4372            notifyUIConversation(uri);
4373        }
4374        // TODO: Ideally, also test that the values actually changed.
4375        if (values.containsKey(MessageColumns.FLAG_READ) ||
4376                values.containsKey(MessageColumns.MAILBOX_KEY)) {
4377            final Cursor c = query(
4378                    Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(),
4379                    MESSAGE_KEYS_PROJECTION, null, null, null);
4380            if (c != null) {
4381                try {
4382                    if (c.moveToFirst()) {
4383                        notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN),
4384                                c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN));
4385                    }
4386                } finally {
4387                    c.close();
4388                }
4389            }
4390        }
4391    }
4392
4393    /**
4394     * Perform a "Delete" operation
4395     * @param uri message to delete
4396     * @return number of rows affected
4397     */
4398    private int uiDeleteMessage(Uri uri) {
4399        final Context context = getContext();
4400        Message msg = getMessageFromLastSegment(uri);
4401        if (msg == null) return 0;
4402        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
4403        if (mailbox == null) return 0;
4404        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
4405            // We actually delete these, including attachments
4406            AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
4407            final int r = context.getContentResolver().delete(
4408                    ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null);
4409            notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
4410            return r;
4411        }
4412        Mailbox trashMailbox =
4413                Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
4414        if (trashMailbox == null) {
4415            return 0;
4416        }
4417        ContentValues values = new ContentValues();
4418        values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
4419        final int r = uiUpdateMessage(uri, values, true);
4420        notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
4421        return r;
4422    }
4423
4424    public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
4425    public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
4426    // Currently unused
4427    //public static final String PICKER_MESSAGE_ID = "picker_message_id";
4428    public static final String PICKER_HEADER_ID = "picker_header_id";
4429
4430    private int pickFolder(Uri uri, int type, int headerId) {
4431        Context context = getContext();
4432        Long acctId = Long.parseLong(uri.getLastPathSegment());
4433        // For push imap, for example, we want the user to select the trash mailbox
4434        Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION,
4435                null, null, null);
4436        try {
4437            if (ac.moveToFirst()) {
4438                final com.android.mail.providers.Account uiAccount =
4439                        new com.android.mail.providers.Account(ac);
4440                Intent intent = new Intent(context, FolderPickerActivity.class);
4441                intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
4442                intent.putExtra(PICKER_MAILBOX_TYPE, type);
4443                intent.putExtra(PICKER_HEADER_ID, headerId);
4444                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4445                context.startActivity(intent);
4446                return 1;
4447            }
4448            return 0;
4449        } finally {
4450            ac.close();
4451        }
4452    }
4453
4454    private int pickTrashFolder(Uri uri) {
4455        return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
4456    }
4457
4458    private int pickSentFolder(Uri uri) {
4459        return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
4460    }
4461
4462    private Cursor uiUndo(String[] projection) {
4463        // First see if we have any operations saved
4464        // TODO: Make sure seq matches
4465        if (!mLastSequenceOps.isEmpty()) {
4466            try {
4467                // TODO Always use this projection?  Or what's passed in?
4468                // Not sure if UI wants it, but I'm making a cursor of convo uri's
4469                MatrixCursor c = new MatrixCursorWithCachedColumns(
4470                        new String[] {UIProvider.ConversationColumns.URI},
4471                        mLastSequenceOps.size());
4472                for (ContentProviderOperation op: mLastSequenceOps) {
4473                    c.addRow(new String[] {op.getUri().toString()});
4474                }
4475                // Just apply the batch and we're done!
4476                applyBatch(mLastSequenceOps);
4477                // But clear the operations
4478                mLastSequenceOps.clear();
4479                return c;
4480            } catch (OperationApplicationException e) {
4481                LogUtils.d(TAG, "applyBatch exception");
4482            }
4483        }
4484        return new MatrixCursorWithCachedColumns(projection, 0);
4485    }
4486
4487    private void notifyUIConversation(Uri uri) {
4488        String id = uri.getLastPathSegment();
4489        Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
4490        if (msg != null) {
4491            notifyUIConversationMailbox(msg.mMailboxKey);
4492        }
4493    }
4494
4495    /**
4496     * Notify about the Mailbox id passed in
4497     * @param id the Mailbox id to be notified
4498     */
4499    private void notifyUIConversationMailbox(long id) {
4500        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
4501        Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id);
4502        if (mailbox == null) {
4503            LogUtils.w(TAG, "No mailbox for notification: " + id);
4504            return;
4505        }
4506        // Notify combined inbox...
4507        if (mailbox.mType == Mailbox.TYPE_INBOX) {
4508            notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER,
4509                    EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX));
4510        }
4511        notifyWidgets(id);
4512    }
4513
4514    /**
4515     * Notify about the Account id passed in
4516     * @param id the Account id to be notified
4517     */
4518    private void notifyUIAccount(long id) {
4519        // Notify on the specific account
4520        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id));
4521
4522        // Notify on the all accounts list
4523        notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
4524    }
4525
4526    /**
4527     * Notify about a folder update. Because folder changes can affect the conversation cursor's
4528     * extras, the conversation must also be notified here.
4529     * @param folderId the folder id to be notified
4530     * @param accountId the account id to be notified (for folder list notification).
4531     */
4532    private void notifyUIFolder(final String folderId, final long accountId) {
4533        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
4534        notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId);
4535        if (accountId != Account.NO_ACCOUNT) {
4536            notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
4537        }
4538
4539        // Notify for combined account too
4540        // TODO: might be nice to only notify when an inbox changes
4541        notifyUI(UIPROVIDER_FOLDER_NOTIFIER,
4542                getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX));
4543        notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID);
4544    }
4545
4546    private void notifyUIFolder(final long folderId, final long accountId) {
4547        notifyUIFolder(Long.toString(folderId), accountId);
4548    }
4549
4550    private void notifyUI(Uri uri, String id) {
4551        final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri;
4552        getContext().getContentResolver().notifyChange(notifyUri, null);
4553    }
4554
4555    private void notifyUI(Uri uri, long id) {
4556        notifyUI(uri, Long.toString(id));
4557    }
4558
4559    private Mailbox getMailbox(final Uri uri) {
4560        final long id = Long.parseLong(uri.getLastPathSegment());
4561        return Mailbox.restoreMailboxWithId(getContext(), id);
4562    }
4563
4564    /**
4565     * Create an android.accounts.Account object for this account.
4566     * @param accountId id of account to load.
4567     * @return an android.accounts.Account for this account, or null if we can't load it.
4568     */
4569    private android.accounts.Account getAccountManagerAccount(final long accountId) {
4570        final Context context = getContext();
4571        final Account account = Account.restoreAccountWithId(context, accountId);
4572        if (account == null) return null;
4573        EmailServiceInfo info =
4574                EmailServiceUtils.getServiceInfo(context, account.getProtocol(context));
4575        return new android.accounts.Account(account.mEmailAddress, info.accountType);
4576    }
4577
4578    /**
4579     * Update an account's periodic sync if the sync interval has changed.
4580     * @param accountId id for the account to update.
4581     * @param values the ContentValues for this update to the account.
4582     */
4583    private void updateAccountSyncInterval(final long accountId, final ContentValues values) {
4584        final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL);
4585        if (syncInterval == null) {
4586            // No change to the sync interval.
4587            return;
4588        }
4589        final android.accounts.Account account = getAccountManagerAccount(accountId);
4590        if (account == null) {
4591            // Unable to load the account, or unknown protocol.
4592            return;
4593        }
4594
4595        LogUtils.d(TAG, "Setting sync interval for account " + accountId + " to " + syncInterval +
4596                " minutes");
4597
4598        // First remove all existing periodic syncs.
4599        final List<PeriodicSync> syncs =
4600                ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY);
4601        for (final PeriodicSync sync : syncs) {
4602            ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras);
4603        }
4604
4605        // Only positive values of sync interval indicate periodic syncs. The value is in minutes,
4606        // while addPeriodicSync expects its time in seconds.
4607        if (syncInterval > 0) {
4608            ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY,
4609                    syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
4610        }
4611    }
4612
4613    private void startSync(final Mailbox mailbox, final int deltaMessageCount) {
4614        android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey);
4615        Bundle extras = new Bundle(7);
4616        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
4617        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
4618        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
4619        extras.putLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, mailbox.mId);
4620        if (deltaMessageCount != 0) {
4621            extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
4622        }
4623        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
4624                EmailContent.CONTENT_URI.toString());
4625        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
4626                SYNC_STATUS_CALLBACK_METHOD);
4627        ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
4628    }
4629
4630    private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) {
4631        if (mailbox != null) {
4632            startSync(mailbox, deltaMessageCount);
4633        }
4634        return null;
4635    }
4636
4637    //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
4638    public static final int VISIBLE_LIMIT_INCREMENT = 10;
4639    //Number of additional messages to load when a user selects "Load more..." in a search
4640    public static final int SEARCH_MORE_INCREMENT = 10;
4641
4642    private Cursor uiFolderLoadMore(final Mailbox mailbox) {
4643        if (mailbox == null) return null;
4644        if (mailbox.mType == Mailbox.TYPE_SEARCH) {
4645            // Ask for 10 more messages
4646            mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
4647            runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId);
4648        } else {
4649            uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT);
4650        }
4651        return null;
4652    }
4653
4654    private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
4655    private SearchParams mSearchParams;
4656
4657    /**
4658     * Returns the search mailbox for the specified account, creating one if necessary
4659     * @return the search mailbox for the passed in account
4660     */
4661    private Mailbox getSearchMailbox(long accountId) {
4662        Context context = getContext();
4663        Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
4664        if (m == null) {
4665            m = new Mailbox();
4666            m.mAccountKey = accountId;
4667            m.mServerId = SEARCH_MAILBOX_SERVER_ID;
4668            m.mFlagVisible = false;
4669            m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
4670            m.mSyncInterval = 0;
4671            m.mType = Mailbox.TYPE_SEARCH;
4672            m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
4673            m.mParentKey = Mailbox.NO_MAILBOX;
4674            m.save(context);
4675        }
4676        return m;
4677    }
4678
4679    private void runSearchQuery(final Context context, final long accountId,
4680            final long searchMailboxId) {
4681        LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d",
4682                accountId, searchMailboxId);
4683
4684        // Start the search running in the background
4685        new AsyncTask<Void, Void, Void>() {
4686            @Override
4687            public Void doInBackground(Void... params) {
4688                final EmailServiceProxy service =
4689                        EmailServiceUtils.getServiceForAccount(context, accountId);
4690                if (service != null) {
4691                    try {
4692                        // Save away the total count
4693                        mSearchParams.mTotalCount =
4694                                service.searchMessages(accountId, mSearchParams, searchMailboxId);
4695                        LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d",
4696                                mSearchParams.mTotalCount);
4697                        notifyUIFolder(searchMailboxId, accountId);
4698                    } catch (RemoteException e) {
4699                        LogUtils.e("searchMessages", "RemoteException", e);
4700                    }
4701                }
4702                return null;
4703            }
4704        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
4705    }
4706
4707    // TODO: Handle searching for more...
4708    private Cursor uiSearch(Uri uri, String[] projection) {
4709        LogUtils.d(TAG, "runSearchQuery in search %s", uri);
4710        final long accountId = Long.parseLong(uri.getLastPathSegment());
4711
4712        // TODO: Check the actual mailbox
4713        Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
4714        if (inbox == null) {
4715            LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account "
4716                    + accountId);
4717
4718            return null;
4719        }
4720
4721        String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
4722        if (filter == null) {
4723            throw new IllegalArgumentException("No query parameter in search query");
4724        }
4725
4726        // Find/create our search mailbox
4727        Mailbox searchMailbox = getSearchMailbox(accountId);
4728        final long searchMailboxId = searchMailbox.mId;
4729
4730        mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
4731
4732        final Context context = getContext();
4733        if (mSearchParams.mOffset == 0) {
4734            LogUtils.d(TAG, "deleting existing search results.");
4735
4736            // Delete existing contents of search mailbox
4737            ContentResolver resolver = context.getContentResolver();
4738            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
4739                    null);
4740            final ContentValues cv = new ContentValues();
4741            // For now, use the actual query as the name of the mailbox
4742            cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
4743            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
4744                    cv, null, null);
4745        }
4746
4747        // Start the search running in the background
4748        runSearchQuery(context, accountId, searchMailboxId);
4749
4750        // This will look just like a "normal" folder
4751        return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
4752                searchMailbox.mId), projection, false);
4753    }
4754
4755    private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
4756
4757    /**
4758     * Delete an account and clean it up
4759     */
4760    private int uiDeleteAccount(Uri uri) {
4761        Context context = getContext();
4762        long accountId = Long.parseLong(uri.getLastPathSegment());
4763        try {
4764            // Get the account URI.
4765            final Account account = Account.restoreAccountWithId(context, accountId);
4766            if (account == null) {
4767                return 0; // Already deleted?
4768            }
4769
4770            deleteAccountData(context, accountId);
4771
4772            // Now delete the account itself
4773            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
4774            context.getContentResolver().delete(uri, null, null);
4775
4776            // Clean up
4777            AccountBackupRestore.backup(context);
4778            SecurityPolicy.getInstance(context).reducePolicies();
4779            MailActivityEmail.setServicesEnabledSync(context);
4780            return 1;
4781        } catch (Exception e) {
4782            LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e);
4783        }
4784        return 0;
4785    }
4786
4787    private int uiDeleteAccountData(Uri uri) {
4788        Context context = getContext();
4789        long accountId = Long.parseLong(uri.getLastPathSegment());
4790        // Get the account URI.
4791        final Account account = Account.restoreAccountWithId(context, accountId);
4792        if (account == null) {
4793            return 0; // Already deleted?
4794        }
4795        deleteAccountData(context, accountId);
4796        return 1;
4797    }
4798
4799    /** Projection used for getting email address for an account. */
4800    private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
4801
4802    private static void deleteAccountData(Context context, long accountId) {
4803        // We will delete PIM data, but by the time the asynchronous call to do that happens,
4804        // the account may have been deleted from the DB. Therefore we have to get the email
4805        // address now and send that, rather than the account id.
4806        final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI,
4807                ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
4808                new String[] {Long.toString(accountId)}, null, 0);
4809        if (emailAddress == null) {
4810            LogUtils.e(TAG, "Could not find email address for account %d", accountId);
4811        }
4812
4813        // Delete synced attachments
4814        AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
4815
4816        // Delete all mailboxes.
4817        ContentResolver resolver = context.getContentResolver();
4818        String[] accountIdArgs = new String[] { Long.toString(accountId) };
4819        resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
4820
4821        // Delete account sync key.
4822        final ContentValues cv = new ContentValues();
4823        cv.putNull(Account.SYNC_KEY);
4824        resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
4825
4826        // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
4827        if (emailAddress != null) {
4828            final IEmailService service =
4829                    EmailServiceUtils.getServiceForAccount(context, accountId);
4830            if (service != null) {
4831                try {
4832                    service.deleteAccountPIMData(emailAddress);
4833                } catch (final RemoteException e) {
4834                    // Can't do anything about this
4835                }
4836            }
4837        }
4838    }
4839
4840    private int[] mSavedWidgetIds = new int[0];
4841    private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>();
4842    private AppWidgetManager mAppWidgetManager;
4843    private ComponentName mEmailComponent;
4844
4845    private void notifyWidgets(long mailboxId) {
4846        Context context = getContext();
4847        // Lazily initialize these
4848        if (mAppWidgetManager == null) {
4849            mAppWidgetManager = AppWidgetManager.getInstance(context);
4850            mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME);
4851        }
4852
4853        // See if we have to populate our array of mailboxes used in widgets
4854        int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent);
4855        if (!Arrays.equals(widgetIds, mSavedWidgetIds)) {
4856            mSavedWidgetIds = widgetIds;
4857            String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds);
4858            // widgetInfo now has pairs of account uri/folder uri
4859            mWidgetNotifyMailboxes.clear();
4860            for (String[] widgetInfo: widgetInfos) {
4861                try {
4862                    if (widgetInfo == null) continue;
4863                    long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment());
4864                    if (!isCombinedMailbox(id)) {
4865                        // For a regular mailbox, just add it to the list
4866                        if (!mWidgetNotifyMailboxes.contains(id)) {
4867                            mWidgetNotifyMailboxes.add(id);
4868                        }
4869                    } else {
4870                        switch (getVirtualMailboxType(id)) {
4871                            // We only handle the combined inbox in widgets
4872                            case Mailbox.TYPE_INBOX:
4873                                Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
4874                                        MailboxColumns.TYPE + "=?",
4875                                        new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null);
4876                                try {
4877                                    while (c.moveToNext()) {
4878                                        mWidgetNotifyMailboxes.add(
4879                                                c.getLong(Mailbox.ID_PROJECTION_COLUMN));
4880                                    }
4881                                } finally {
4882                                    c.close();
4883                                }
4884                                break;
4885                        }
4886                    }
4887                } catch (NumberFormatException e) {
4888                    // Move along
4889                }
4890            }
4891        }
4892
4893        // If our mailbox needs to be notified, do so...
4894        if (mWidgetNotifyMailboxes.contains(mailboxId)) {
4895            Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED);
4896            intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId));
4897            intent.setType(EMAIL_APP_MIME_TYPE);
4898            context.sendBroadcast(intent);
4899         }
4900    }
4901
4902    @Override
4903    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
4904        Context context = getContext();
4905        writer.println("Installed services:");
4906        for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) {
4907            writer.println("  " + info);
4908        }
4909        writer.println();
4910        writer.println("Accounts: ");
4911        Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
4912        if (cursor.getCount() == 0) {
4913            writer.println("  None");
4914        }
4915        try {
4916            while (cursor.moveToNext()) {
4917                Account account = new Account();
4918                account.restore(cursor);
4919                writer.println("  Account " + account.mDisplayName);
4920                HostAuth hostAuth =
4921                        HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
4922                if (hostAuth != null) {
4923                    writer.println("    Protocol = " + hostAuth.mProtocol +
4924                            (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " +
4925                                    account.mProtocolVersion));
4926                }
4927            }
4928        } finally {
4929            cursor.close();
4930        }
4931    }
4932}
4933