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