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.accounts.AccountManager;
20import android.appwidget.AppWidgetManager;
21import android.content.ComponentCallbacks;
22import android.content.ComponentName;
23import android.content.ContentProvider;
24import android.content.ContentProviderOperation;
25import android.content.ContentProviderResult;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.Intent;
31import android.content.OperationApplicationException;
32import android.content.PeriodicSync;
33import android.content.SharedPreferences;
34import android.content.UriMatcher;
35import android.content.pm.ActivityInfo;
36import android.content.pm.PackageManager;
37import android.content.res.Configuration;
38import android.content.res.Resources;
39import android.database.ContentObserver;
40import android.database.Cursor;
41import android.database.CursorWrapper;
42import android.database.DatabaseUtils;
43import android.database.MatrixCursor;
44import android.database.MergeCursor;
45import android.database.sqlite.SQLiteDatabase;
46import android.database.sqlite.SQLiteException;
47import android.database.sqlite.SQLiteStatement;
48import android.net.Uri;
49import android.os.AsyncTask;
50import android.os.Binder;
51import android.os.Build;
52import android.os.Bundle;
53import android.os.Handler;
54import android.os.Handler.Callback;
55import android.os.Looper;
56import android.os.Parcel;
57import android.os.ParcelFileDescriptor;
58import android.os.RemoteException;
59import android.provider.BaseColumns;
60import android.text.TextUtils;
61import android.text.format.DateUtils;
62import android.util.Base64;
63import android.util.Log;
64import android.util.SparseArray;
65
66import com.android.common.content.ProjectionMap;
67import com.android.email.DebugUtils;
68import com.android.email.NotificationController;
69import com.android.email.NotificationControllerCreatorHolder;
70import com.android.email.Preferences;
71import com.android.email.R;
72import com.android.email.SecurityPolicy;
73import com.android.email.activity.setup.AccountSecurity;
74import com.android.email.activity.setup.AccountSettingsUtils;
75import com.android.email.service.AttachmentService;
76import com.android.email.service.EmailServiceUtils;
77import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
78import com.android.emailcommon.Logging;
79import com.android.emailcommon.mail.Address;
80import com.android.emailcommon.provider.Account;
81import com.android.emailcommon.provider.Credential;
82import com.android.emailcommon.provider.EmailContent;
83import com.android.emailcommon.provider.EmailContent.AccountColumns;
84import com.android.emailcommon.provider.EmailContent.Attachment;
85import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
86import com.android.emailcommon.provider.EmailContent.Body;
87import com.android.emailcommon.provider.EmailContent.BodyColumns;
88import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
89import com.android.emailcommon.provider.EmailContent.MailboxColumns;
90import com.android.emailcommon.provider.EmailContent.Message;
91import com.android.emailcommon.provider.EmailContent.MessageColumns;
92import com.android.emailcommon.provider.EmailContent.PolicyColumns;
93import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
94import com.android.emailcommon.provider.EmailContent.SyncColumns;
95import com.android.emailcommon.provider.HostAuth;
96import com.android.emailcommon.provider.Mailbox;
97import com.android.emailcommon.provider.MailboxUtilities;
98import com.android.emailcommon.provider.MessageChangeLogTable;
99import com.android.emailcommon.provider.MessageMove;
100import com.android.emailcommon.provider.MessageStateChange;
101import com.android.emailcommon.provider.Policy;
102import com.android.emailcommon.provider.QuickResponse;
103import com.android.emailcommon.service.EmailServiceProxy;
104import com.android.emailcommon.service.EmailServiceStatus;
105import com.android.emailcommon.service.IEmailService;
106import com.android.emailcommon.service.SearchParams;
107import com.android.emailcommon.utility.AttachmentUtilities;
108import com.android.emailcommon.utility.EmailAsyncTask;
109import com.android.emailcommon.utility.IntentUtilities;
110import com.android.emailcommon.utility.Utility;
111import com.android.ex.photo.provider.PhotoContract;
112import com.android.mail.preferences.MailPrefs;
113import com.android.mail.preferences.MailPrefs.PreferenceKeys;
114import com.android.mail.providers.Folder;
115import com.android.mail.providers.FolderList;
116import com.android.mail.providers.Settings;
117import com.android.mail.providers.UIProvider;
118import com.android.mail.providers.UIProvider.AccountCapabilities;
119import com.android.mail.providers.UIProvider.AccountColumns.SettingsColumns;
120import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
121import com.android.mail.providers.UIProvider.ConversationPriority;
122import com.android.mail.providers.UIProvider.ConversationSendingState;
123import com.android.mail.providers.UIProvider.DraftType;
124import com.android.mail.utils.AttachmentUtils;
125import com.android.mail.utils.LogTag;
126import com.android.mail.utils.LogUtils;
127import com.android.mail.utils.MatrixCursorWithCachedColumns;
128import com.android.mail.utils.MatrixCursorWithExtra;
129import com.android.mail.utils.MimeType;
130import com.android.mail.utils.Utils;
131import com.android.mail.widget.BaseWidgetProvider;
132import com.google.common.collect.ImmutableMap;
133import com.google.common.collect.ImmutableSet;
134import com.google.common.collect.Sets;
135
136import java.io.File;
137import java.io.FileDescriptor;
138import java.io.FileNotFoundException;
139import java.io.FileWriter;
140import java.io.IOException;
141import java.io.PrintWriter;
142import java.util.ArrayList;
143import java.util.Arrays;
144import java.util.Collection;
145import java.util.HashSet;
146import java.util.List;
147import java.util.Locale;
148import java.util.Map;
149import java.util.Set;
150import java.util.regex.Pattern;
151
152public class EmailProvider extends ContentProvider
153        implements SharedPreferences.OnSharedPreferenceChangeListener {
154
155    private static final String TAG = LogTag.getLogTag();
156
157    // Time to delay upsync requests.
158    public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
159
160    public static String EMAIL_APP_MIME_TYPE;
161
162    // exposed for testing
163    public static final String DATABASE_NAME = "EmailProvider.db";
164    public static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
165
166    // We don't back up to the backup database anymore, just keep this constant here so we can
167    // delete the old backups and trigger a new backup to the account manager
168    @Deprecated
169    private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db";
170    private static final String ACCOUNT_MANAGER_JSON_TAG = "accountJson";
171
172
173    private static final String PREFERENCE_FRAGMENT_CLASS_NAME =
174            "com.android.email.activity.setup.AccountSettingsFragment";
175    /**
176     * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this
177     * {@link android.content.Intent} and update accordingly. However, this can be very broad and
178     * is NOT the preferred way of getting notification.
179     */
180    private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED =
181        "com.android.email.MESSAGE_LIST_DATASET_CHANGED";
182
183    private static final String EMAIL_MESSAGE_MIME_TYPE =
184        "vnd.android.cursor.item/email-message";
185    private static final String EMAIL_ATTACHMENT_MIME_TYPE =
186        "vnd.android.cursor.item/email-attachment";
187
188    /** Appended to the notification URI for delete operations */
189    private static final String NOTIFICATION_OP_DELETE = "delete";
190    /** Appended to the notification URI for insert operations */
191    private static final String NOTIFICATION_OP_INSERT = "insert";
192    /** Appended to the notification URI for update operations */
193    private static final String NOTIFICATION_OP_UPDATE = "update";
194
195    /** The query string to trigger a folder refresh. */
196    protected static String QUERY_UIREFRESH = "uirefresh";
197
198    // Definitions for our queries looking for orphaned messages
199    private static final String[] ORPHANS_PROJECTION
200        = new String[] {MessageColumns._ID, MessageColumns.MAILBOX_KEY};
201    private static final int ORPHANS_ID = 0;
202    private static final int ORPHANS_MAILBOX_KEY = 1;
203
204    private static final String WHERE_ID = BaseColumns._ID + "=?";
205
206    private static final int ACCOUNT_BASE = 0;
207    private static final int ACCOUNT = ACCOUNT_BASE;
208    private static final int ACCOUNT_ID = ACCOUNT_BASE + 1;
209    private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 2;
210    private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 3;
211    private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 4;
212
213    private static final int MAILBOX_BASE = 0x1000;
214    private static final int MAILBOX = MAILBOX_BASE;
215    private static final int MAILBOX_ID = MAILBOX_BASE + 1;
216    private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2;
217    private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3;
218    private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4;
219
220    private static final int MESSAGE_BASE = 0x2000;
221    private static final int MESSAGE = MESSAGE_BASE;
222    private static final int MESSAGE_ID = MESSAGE_BASE + 1;
223    private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2;
224    private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3;
225    private static final int MESSAGE_MOVE = MESSAGE_BASE + 4;
226    private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5;
227
228    private static final int ATTACHMENT_BASE = 0x3000;
229    private static final int ATTACHMENT = ATTACHMENT_BASE;
230    private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1;
231    private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2;
232    private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3;
233
234    private static final int HOSTAUTH_BASE = 0x4000;
235    private static final int HOSTAUTH = HOSTAUTH_BASE;
236    private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1;
237
238    private static final int UPDATED_MESSAGE_BASE = 0x5000;
239    private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE;
240    private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1;
241
242    private static final int DELETED_MESSAGE_BASE = 0x6000;
243    private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE;
244    private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1;
245
246    private static final int POLICY_BASE = 0x7000;
247    private static final int POLICY = POLICY_BASE;
248    private static final int POLICY_ID = POLICY_BASE + 1;
249
250    private static final int QUICK_RESPONSE_BASE = 0x8000;
251    private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE;
252    private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1;
253    private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2;
254
255    private static final int UI_BASE = 0x9000;
256    private static final int UI_FOLDERS = UI_BASE;
257    private static final int UI_SUBFOLDERS = UI_BASE + 1;
258    private static final int UI_MESSAGES = UI_BASE + 2;
259    private static final int UI_MESSAGE = UI_BASE + 3;
260    private static final int UI_UNDO = UI_BASE + 4;
261    private static final int UI_FOLDER_REFRESH = UI_BASE + 5;
262    private static final int UI_FOLDER = UI_BASE + 6;
263    private static final int UI_ACCOUNT = UI_BASE + 7;
264    private static final int UI_ACCTS = UI_BASE + 8;
265    private static final int UI_ATTACHMENTS = UI_BASE + 9;
266    private static final int UI_ATTACHMENT = UI_BASE + 10;
267    private static final int UI_ATTACHMENT_BY_CID = UI_BASE + 11;
268    private static final int UI_SEARCH = UI_BASE + 12;
269    private static final int UI_ACCOUNT_DATA = UI_BASE + 13;
270    private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 14;
271    private static final int UI_CONVERSATION = UI_BASE + 15;
272    private static final int UI_RECENT_FOLDERS = UI_BASE + 16;
273    private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 17;
274    private static final int UI_FULL_FOLDERS = UI_BASE + 18;
275    private static final int UI_ALL_FOLDERS = UI_BASE + 19;
276    private static final int UI_PURGE_FOLDER = UI_BASE + 20;
277    private static final int UI_INBOX = UI_BASE + 21;
278    private static final int UI_ACCTSETTINGS = UI_BASE + 22;
279
280    private static final int BODY_BASE = 0xA000;
281    private static final int BODY = BODY_BASE;
282    private static final int BODY_ID = BODY_BASE + 1;
283    private static final int BODY_HTML = BODY_BASE + 2;
284    private static final int BODY_TEXT = BODY_BASE + 3;
285
286    private static final int CREDENTIAL_BASE = 0xB000;
287    private static final int CREDENTIAL = CREDENTIAL_BASE;
288    private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1;
289
290    private static final int BASE_SHIFT = 12;  // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
291
292    private static final SparseArray<String> TABLE_NAMES;
293    static {
294        SparseArray<String> array = new SparseArray<String>(11);
295        array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME);
296        array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME);
297        array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME);
298        array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME);
299        array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME);
300        array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME);
301        array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME);
302        array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME);
303        array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME);
304        array.put(UI_BASE >> BASE_SHIFT, null);
305        array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME);
306        array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME);
307        TABLE_NAMES = array;
308    }
309
310    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
311
312    /**
313     * Functions which manipulate the database connection or files synchronize on this.
314     * It's static because there can be multiple provider objects.
315     * TODO: Do we actually need to synchronize across all DB access, not just connection creation?
316     */
317    private static final Object sDatabaseLock = new Object();
318
319    /**
320     * Let's only generate these SQL strings once, as they are used frequently
321     * Note that this isn't relevant for table creation strings, since they are used only once
322     */
323    private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " +
324        Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
325        BaseColumns._ID + '=';
326
327    private static final String UPDATED_MESSAGE_DELETE = "delete from " +
328        Message.UPDATED_TABLE_NAME + " where " + BaseColumns._ID + '=';
329
330    private static final String DELETED_MESSAGE_INSERT = "insert or replace into " +
331        Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
332        BaseColumns._ID + '=';
333
334    private static final String ORPHAN_BODY_MESSAGE_ID_SELECT =
335            "select " + BodyColumns.MESSAGE_KEY + " from " + Body.TABLE_NAME +
336                    " except select " + BaseColumns._ID + " from " + Message.TABLE_NAME;
337
338    private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
339        " where " + BodyColumns.MESSAGE_KEY + " in " + '(' + ORPHAN_BODY_MESSAGE_ID_SELECT + ')';
340
341    private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
342        " where " + BodyColumns.MESSAGE_KEY + '=';
343
344    private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
345
346    private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId";
347
348    // For undo handling
349    private int mLastSequence = -1;
350    private final ArrayList<ContentProviderOperation> mLastSequenceOps =
351            new ArrayList<ContentProviderOperation>();
352
353    // Query parameter indicating the command came from UIProvider
354    private static final String IS_UIPROVIDER = "is_uiprovider";
355
356    private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status";
357
358    private static final String[] MIME_TYPE_PROJECTION = new String[]{AttachmentColumns.MIME_TYPE};
359
360    private static final String[] CACHED_FILE_QUERY_PROJECTION = new String[]
361            { AttachmentColumns._ID, AttachmentColumns.FILENAME, AttachmentColumns.SIZE,
362                    AttachmentColumns.CONTENT_URI };
363
364    /**
365     * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in
366     * @param uri the Uri to match
367     * @return the match value
368     */
369    private static int findMatch(Uri uri, String methodName) {
370        int match = sURIMatcher.match(uri);
371        if (match < 0) {
372            throw new IllegalArgumentException("Unknown uri: " + uri);
373        } else if (Logging.LOGD) {
374            LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
375        }
376        return match;
377    }
378
379    // exposed for testing
380    public static Uri INTEGRITY_CHECK_URI;
381
382    public static Uri ACCOUNT_BACKUP_URI;
383    private static Uri FOLDER_STATUS_URI;
384
385    private SQLiteDatabase mDatabase;
386    private SQLiteDatabase mBodyDatabase;
387
388    private Handler mDelayedSyncHandler;
389    private final Set<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>();
390
391    private static void reconcileAccountsAsync(final Context context) {
392        if (context.getResources().getBoolean(R.bool.reconcile_accounts)) {
393            EmailAsyncTask.runAsyncParallel(new Runnable() {
394                @Override
395                public void run() {
396                    AccountReconciler.reconcileAccounts(context);
397                }
398            });
399        }
400    }
401
402    public static Uri uiUri(String type, long id) {
403        return Uri.parse(uiUriString(type, id));
404    }
405
406    /**
407     * Creates a URI string from a database ID (guaranteed to be unique).
408     * @param type of the resource: uifolder, message, etc.
409     * @param id the id of the resource.
410     * @return uri string
411     */
412    public static String uiUriString(String type, long id) {
413        return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id));
414    }
415
416    /**
417     * Orphan record deletion utility.  Generates a sqlite statement like:
418     *  delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>)
419     * Exposed for testing.
420     * @param db the EmailProvider database
421     * @param table the table whose orphans are to be removed
422     * @param column the column deletion will be based on
423     * @param foreignColumn the column in the foreign table whose absence will trigger the deletion
424     * @param foreignTable the foreign table
425     */
426    public static void deleteUnlinked(SQLiteDatabase db, String table, String column,
427            String foreignColumn, String foreignTable) {
428        int count = db.delete(table, column + " not in (select " + foreignColumn + " from " +
429                foreignTable + ")", null);
430        if (count > 0) {
431            LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table);
432        }
433    }
434
435
436    /**
437     * Make sure that parentKeys match with parentServerId.
438     * When we sync folders, we do two passes: First to create the mailbox rows, and second
439     * to set the parentKeys. Two passes are needed because we won't know the parent's Id
440     * until that row is inserted, and the order in which the rows are given is arbitrary.
441     * If we crash while this operation is in progress, the parent keys can be left uninitialized.
442     * @param db SQLiteDatabase to modify
443     */
444    private void fixParentKeys(SQLiteDatabase db) {
445        LogUtils.d(TAG, "Fixing parent keys");
446
447        // Update the parentKey for each mailbox row to match the _id of the row whose
448        // serverId matches our parentServerId. This will leave parentKey blank for any
449        // row that does not have a parentServerId
450
451        // This is kind of a confusing sql statement, so here's the actual text of it,
452        // for reference:
453        //
454        //   update mailbox set parentKey = (select _id from mailbox as b where
455        //   mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and
456        //   mailbox.accountKey=b.accountKey)
457        db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "="
458                + "(select " + Mailbox._ID + " from " + Mailbox.TABLE_NAME + " as b where "
459                + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "="
460                + "b." + MailboxColumns.SERVER_ID + " and "
461                + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and "
462                + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY
463                + "=b." + Mailbox.ACCOUNT_KEY + ")");
464
465        // Top level folders can still have uninitialized parent keys. Update these
466        // to indicate that the parent is -1.
467        //
468        //   update mailbox set parentKey = -1 where parentKey=0 or parentKey is null;
469        db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY
470                + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY
471                + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY
472                + " is null");
473
474    }
475
476    // exposed for testing
477    public SQLiteDatabase getDatabase(Context context) {
478        synchronized (sDatabaseLock) {
479            // Always return the cached database, if we've got one
480            if (mDatabase != null) {
481                return mDatabase;
482            }
483
484            // Whenever we create or re-cache the databases, make sure that we haven't lost one
485            // to corruption
486            checkDatabases();
487
488            DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME);
489            mDatabase = helper.getWritableDatabase();
490            DBHelper.BodyDatabaseHelper bodyHelper =
491                    new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME);
492            mBodyDatabase = bodyHelper.getWritableDatabase();
493            if (mBodyDatabase != null) {
494                String bodyFileName = mBodyDatabase.getPath();
495                mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
496            }
497
498            // Restore accounts if the database is corrupted...
499            restoreIfNeeded(context, mDatabase);
500            // Check for any orphaned Messages in the updated/deleted tables
501            deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
502            deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME);
503            // Delete orphaned mailboxes/messages/policies (account no longer exists)
504            deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY,
505                    AccountColumns._ID, Account.TABLE_NAME);
506            deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY,
507                    AccountColumns._ID, Account.TABLE_NAME);
508            deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns._ID,
509                    AccountColumns.POLICY_KEY, Account.TABLE_NAME);
510            fixParentKeys(mDatabase);
511            initUiProvider();
512            return mDatabase;
513        }
514    }
515
516    /**
517     * Perform startup actions related to UI
518     */
519    private void initUiProvider() {
520        // Clear mailbox sync status
521        mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS +
522                "=" + UIProvider.SyncStatus.NO_SYNC);
523    }
524
525    /**
526     * Restore user Account and HostAuth data from our backup database
527     */
528    private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) {
529        if (DebugUtils.DEBUG) {
530            LogUtils.w(TAG, "restoreIfNeeded...");
531        }
532        // Check for legacy backup
533        String legacyBackup = Preferences.getLegacyBackupPreference(context);
534        // If there's a legacy backup, create a new-style backup and delete the legacy backup
535        // In the 1:1000000000 chance that the user gets an app update just as his database becomes
536        // corrupt, oh well...
537        if (!TextUtils.isEmpty(legacyBackup)) {
538            backupAccounts(context, mainDatabase);
539            Preferences.clearLegacyBackupPreference(context);
540            LogUtils.w(TAG, "Created new EmailProvider backup database");
541            return;
542        }
543
544        // If there's a backup database (old style) delete it and trigger an account manager backup.
545        // Roughly the same comment as above applies
546        final File backupDb = context.getDatabasePath(BACKUP_DATABASE_NAME);
547        if (backupDb.exists()) {
548            backupAccounts(context, mainDatabase);
549            context.deleteDatabase(BACKUP_DATABASE_NAME);
550            LogUtils.w(TAG, "Migrated from backup database to account manager");
551            return;
552        }
553
554        // If we have accounts, we're done
555        if (DatabaseUtils.longForQuery(mainDatabase,
556                                      "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )",
557                                      EmailContent.ID_PROJECTION) > 0) {
558            if (DebugUtils.DEBUG) {
559                LogUtils.w(TAG, "restoreIfNeeded: Account exists.");
560            }
561            return;
562        }
563
564        restoreAccounts(context);
565    }
566
567    /** {@inheritDoc} */
568    @Override
569    public void shutdown() {
570        if (mDatabase != null) {
571            mDatabase.close();
572            mDatabase = null;
573        }
574        if (mBodyDatabase != null) {
575            mBodyDatabase.close();
576            mBodyDatabase = null;
577        }
578    }
579
580    // exposed for testing
581    public static void deleteMessageOrphans(SQLiteDatabase database, String tableName) {
582        if (database != null) {
583            // We'll look at all of the items in the table; there won't be many typically
584            Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null);
585            // Usually, there will be nothing in these tables, so make a quick check
586            try {
587                if (c.getCount() == 0) return;
588                ArrayList<Long> foundMailboxes = new ArrayList<Long>();
589                ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
590                ArrayList<Long> deleteList = new ArrayList<Long>();
591                String[] bindArray = new String[1];
592                while (c.moveToNext()) {
593                    // Get the mailbox key and see if we've already found this mailbox
594                    // If so, we're fine
595                    long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
596                    // If we already know this mailbox doesn't exist, mark the message for deletion
597                    if (notFoundMailboxes.contains(mailboxId)) {
598                        deleteList.add(c.getLong(ORPHANS_ID));
599                    // If we don't know about this mailbox, we'll try to find it
600                    } else if (!foundMailboxes.contains(mailboxId)) {
601                        bindArray[0] = Long.toString(mailboxId);
602                        Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
603                                Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
604                        try {
605                            // If it exists, we'll add it to the "found" mailboxes
606                            if (boxCursor.moveToFirst()) {
607                                foundMailboxes.add(mailboxId);
608                            // Otherwise, we'll add to "not found" and mark the message for deletion
609                            } else {
610                                notFoundMailboxes.add(mailboxId);
611                                deleteList.add(c.getLong(ORPHANS_ID));
612                            }
613                        } finally {
614                            boxCursor.close();
615                        }
616                    }
617                }
618                // Now, delete the orphan messages
619                for (long messageId: deleteList) {
620                    bindArray[0] = Long.toString(messageId);
621                    database.delete(tableName, WHERE_ID, bindArray);
622                }
623            } finally {
624                c.close();
625            }
626        }
627    }
628
629    @Override
630    public int delete(Uri uri, String selection, String[] selectionArgs) {
631        Log.d(TAG, "Delete: " + uri);
632        final int match = findMatch(uri, "delete");
633        final Context context = getContext();
634        // Pick the correct database for this operation
635        // If we're in a transaction already (which would happen during applyBatch), then the
636        // body database is already attached to the email database and any attempt to use the
637        // body database directly will result in a SQLiteException (the database is locked)
638        final SQLiteDatabase db = getDatabase(context);
639        final int table = match >> BASE_SHIFT;
640        String id = "0";
641        boolean messageDeletion = false;
642
643        final String tableName = TABLE_NAMES.valueAt(table);
644        int result = -1;
645
646        try {
647            if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
648                if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
649                    notifyUIConversation(uri);
650                }
651            }
652            switch (match) {
653                case UI_MESSAGE:
654                    return uiDeleteMessage(uri);
655                case UI_ACCOUNT_DATA:
656                    return uiDeleteAccountData(uri);
657                case UI_ACCOUNT:
658                    return uiDeleteAccount(uri);
659                case UI_PURGE_FOLDER:
660                    return uiPurgeFolder(uri);
661                case MESSAGE_SELECTION:
662                    Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
663                            selectionArgs, null, null, null);
664                    try {
665                        if (findCursor.moveToFirst()) {
666                            return delete(ContentUris.withAppendedId(
667                                    Message.CONTENT_URI,
668                                    findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
669                                    null, null);
670                        } else {
671                            return 0;
672                        }
673                    } finally {
674                        findCursor.close();
675                    }
676                // These are cases in which one or more Messages might get deleted, either by
677                // cascade or explicitly
678                case MAILBOX_ID:
679                case MAILBOX:
680                case ACCOUNT_ID:
681                case ACCOUNT:
682                case MESSAGE:
683                case SYNCED_MESSAGE_ID:
684                case MESSAGE_ID:
685                    // Handle lost Body records here, since this cannot be done in a trigger
686                    // The process is:
687                    //  1) Begin a transaction, ensuring that both databases are affected atomically
688                    //  2) Do the requested deletion, with cascading deletions handled in triggers
689                    //  3) End the transaction, committing all changes atomically
690                    //
691                    // Bodies are auto-deleted here;  Attachments are auto-deleted via trigger
692                    messageDeletion = true;
693                    db.beginTransaction();
694                    break;
695            }
696            switch (match) {
697                case BODY_ID:
698                case DELETED_MESSAGE_ID:
699                case SYNCED_MESSAGE_ID:
700                case MESSAGE_ID:
701                case UPDATED_MESSAGE_ID:
702                case ATTACHMENT_ID:
703                case MAILBOX_ID:
704                case ACCOUNT_ID:
705                case HOSTAUTH_ID:
706                case POLICY_ID:
707                case QUICK_RESPONSE_ID:
708                case CREDENTIAL_ID:
709                    id = uri.getPathSegments().get(1);
710                    if (match == SYNCED_MESSAGE_ID) {
711                        // For synced messages, first copy the old message to the deleted table and
712                        // delete it from the updated table (in case it was updated first)
713                        // Note that this is all within a transaction, for atomicity
714                        db.execSQL(DELETED_MESSAGE_INSERT + id);
715                        db.execSQL(UPDATED_MESSAGE_DELETE + id);
716                    }
717
718                    final long accountId;
719                    if (match == MAILBOX_ID) {
720                        accountId = Mailbox.getAccountIdForMailbox(context, id);
721                    } else {
722                        accountId = Account.NO_ACCOUNT;
723                    }
724
725                    result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
726
727                    if (match == ACCOUNT_ID) {
728                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
729                        notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
730                    } else if (match == MAILBOX_ID) {
731                        notifyUIFolder(id, accountId);
732                    } else if (match == ATTACHMENT_ID) {
733                        notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
734                    }
735                    break;
736                case ATTACHMENTS_MESSAGE_ID:
737                    // All attachments for the given message
738                    id = uri.getPathSegments().get(2);
739                    result = db.delete(tableName,
740                            whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection),
741                            selectionArgs);
742                    break;
743
744                case BODY:
745                case MESSAGE:
746                case DELETED_MESSAGE:
747                case UPDATED_MESSAGE:
748                case ATTACHMENT:
749                case MAILBOX:
750                case ACCOUNT:
751                case HOSTAUTH:
752                case POLICY:
753                    result = db.delete(tableName, selection, selectionArgs);
754                    break;
755                case MESSAGE_MOVE:
756                    db.delete(MessageMove.TABLE_NAME, selection, selectionArgs);
757                    break;
758                case MESSAGE_STATE_CHANGE:
759                    db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs);
760                    break;
761                default:
762                    throw new IllegalArgumentException("Unknown URI " + uri);
763            }
764            if (messageDeletion) {
765                if (match == MESSAGE_ID) {
766                    // Delete the Body record associated with the deleted message
767                    final long messageId = Long.valueOf(id);
768                    try {
769                        deleteBodyFiles(context, messageId);
770                    } catch (final IllegalStateException e) {
771                        LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
772                    }
773                    db.execSQL(DELETE_BODY + id);
774                } else {
775                    // Delete any orphaned Body records
776                    final Cursor orphans = db.rawQuery(ORPHAN_BODY_MESSAGE_ID_SELECT, null);
777                    try {
778                        while (orphans.moveToNext()) {
779                            final long messageId = orphans.getLong(0);
780                            try {
781                                deleteBodyFiles(context, messageId);
782                            } catch (final IllegalStateException e) {
783                                LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
784                            }
785                        }
786                    } finally {
787                        orphans.close();
788                    }
789                    db.execSQL(DELETE_ORPHAN_BODIES);
790                }
791                db.setTransactionSuccessful();
792            }
793        } catch (SQLiteException e) {
794            checkDatabases();
795            throw e;
796        } finally {
797            if (messageDeletion) {
798                db.endTransaction();
799            }
800        }
801
802        // Notify all notifier cursors
803        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
804
805        // Notify all email content cursors
806        notifyUI(EmailContent.CONTENT_URI, null);
807        return result;
808    }
809
810    @Override
811    // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM)
812    public String getType(Uri uri) {
813        int match = findMatch(uri, "getType");
814        switch (match) {
815            case BODY_ID:
816                return "vnd.android.cursor.item/email-body";
817            case BODY:
818                return "vnd.android.cursor.dir/email-body";
819            case UPDATED_MESSAGE_ID:
820            case MESSAGE_ID:
821                // NOTE: According to the framework folks, we're supposed to invent mime types as
822                // a way of passing information to drag & drop recipients.
823                // If there's a mailboxId parameter in the url, we respond with a mime type that
824                // has -n appended, where n is the mailboxId of the message.  The drag & drop code
825                // uses this information to know not to allow dragging the item to its own mailbox
826                String mimeType = EMAIL_MESSAGE_MIME_TYPE;
827                String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID);
828                if (mailboxId != null) {
829                    mimeType += "-" + mailboxId;
830                }
831                return mimeType;
832            case UPDATED_MESSAGE:
833            case MESSAGE:
834                return "vnd.android.cursor.dir/email-message";
835            case MAILBOX:
836                return "vnd.android.cursor.dir/email-mailbox";
837            case MAILBOX_ID:
838                return "vnd.android.cursor.item/email-mailbox";
839            case ACCOUNT:
840                return "vnd.android.cursor.dir/email-account";
841            case ACCOUNT_ID:
842                return "vnd.android.cursor.item/email-account";
843            case ATTACHMENTS_MESSAGE_ID:
844            case ATTACHMENT:
845                return "vnd.android.cursor.dir/email-attachment";
846            case ATTACHMENT_ID:
847                return EMAIL_ATTACHMENT_MIME_TYPE;
848            case HOSTAUTH:
849                return "vnd.android.cursor.dir/email-hostauth";
850            case HOSTAUTH_ID:
851                return "vnd.android.cursor.item/email-hostauth";
852            case ATTACHMENTS_CACHED_FILE_ACCESS: {
853                SQLiteDatabase db = getDatabase(getContext());
854                Cursor c = db.query(Attachment.TABLE_NAME, MIME_TYPE_PROJECTION,
855                        AttachmentColumns.CACHED_FILE + "=?", new String[]{uri.toString()},
856                        null, null, null, null);
857                try {
858                    if (c != null && c.moveToFirst()) {
859                        return c.getString(0);
860                    } else {
861                        return null;
862                    }
863                } finally {
864                    if (c != null) {
865                        c.close();
866                    }
867                }
868            }
869            default:
870                return null;
871        }
872    }
873
874    // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI
875    // as the base because that gets spammed.
876    // These can't be statically initialized because they depend on EmailContent.AUTHORITY
877    private static Uri UIPROVIDER_CONVERSATION_NOTIFIER;
878    private static Uri UIPROVIDER_FOLDER_NOTIFIER;
879    private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER;
880    private static Uri UIPROVIDER_ACCOUNT_NOTIFIER;
881    // Not currently used
882    //public static Uri UIPROVIDER_SETTINGS_NOTIFIER;
883    private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER;
884    private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER;
885    private static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER;
886    private static Uri UIPROVIDER_MESSAGE_NOTIFIER;
887    private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER;
888
889    @Override
890    public Uri insert(Uri uri, ContentValues values) {
891        Log.d(TAG, "Insert: " + uri);
892        final int match = findMatch(uri, "insert");
893        final Context context = getContext();
894
895        // See the comment at delete(), above
896        final SQLiteDatabase db = getDatabase(context);
897        final int table = match >> BASE_SHIFT;
898        String id = "0";
899        long longId;
900
901        // We do NOT allow setting of unreadCount/messageCount via the provider
902        // These columns are maintained via triggers
903        if (match == MAILBOX_ID || match == MAILBOX) {
904            values.put(MailboxColumns.UNREAD_COUNT, 0);
905            values.put(MailboxColumns.MESSAGE_COUNT, 0);
906        }
907
908        final Uri resultUri;
909
910        try {
911            switch (match) {
912                case BODY:
913                    final ContentValues dbValues = new ContentValues(values);
914                    // Prune out the content we don't want in the DB
915                    dbValues.remove(BodyColumns.HTML_CONTENT);
916                    dbValues.remove(BodyColumns.TEXT_CONTENT);
917                    // TODO: move this to the message table
918                    longId = db.insert(Body.TABLE_NAME, "foo", dbValues);
919                    resultUri = ContentUris.withAppendedId(uri, longId);
920                    // Write content to the filesystem where appropriate
921                    // This will look less ugly once the body table is folded into the message table
922                    // and we can just use longId instead
923                    if (!values.containsKey(BodyColumns.MESSAGE_KEY)) {
924                        throw new IllegalArgumentException(
925                                "Cannot insert body without MESSAGE_KEY");
926                    }
927                    final long messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
928                    // Ensure that no pre-existing body files contaminate the message
929                    deleteBodyFiles(context, messageId);
930                    writeBodyFiles(getContext(), messageId, values);
931                    break;
932                // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
933                // or DELETED_MESSAGE; see the comment below for details
934                case UPDATED_MESSAGE:
935                case DELETED_MESSAGE:
936                case MESSAGE:
937                    decodeEmailAddresses(values);
938                case ATTACHMENT:
939                case MAILBOX:
940                case ACCOUNT:
941                case HOSTAUTH:
942                case CREDENTIAL:
943                case POLICY:
944                case QUICK_RESPONSE:
945                    longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
946                    resultUri = ContentUris.withAppendedId(uri, longId);
947                    switch(match) {
948                        case MESSAGE:
949                            final long mailboxId = values.getAsLong(MessageColumns.MAILBOX_KEY);
950                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
951                                notifyUIConversationMailbox(mailboxId);
952                            }
953                            notifyUIFolder(mailboxId, values.getAsLong(MessageColumns.ACCOUNT_KEY));
954                            break;
955                        case MAILBOX:
956                            if (values.containsKey(MailboxColumns.TYPE)) {
957                                if (values.getAsInteger(MailboxColumns.TYPE) <
958                                        Mailbox.TYPE_NOT_EMAIL) {
959                                    // Notify the account when a new mailbox is added
960                                    final Long accountId =
961                                            values.getAsLong(MailboxColumns.ACCOUNT_KEY);
962                                    if (accountId != null && accountId > 0) {
963                                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId);
964                                        notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
965                                    }
966                                }
967                            }
968                            break;
969                        case ACCOUNT:
970                            updateAccountSyncInterval(longId, values);
971                            if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
972                                notifyUIAccount(longId);
973                            }
974                            notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
975                            break;
976                        case UPDATED_MESSAGE:
977                        case DELETED_MESSAGE:
978                            throw new IllegalArgumentException("Unknown URL " + uri);
979                        case ATTACHMENT:
980                            int flags = 0;
981                            if (values.containsKey(AttachmentColumns.FLAGS)) {
982                                flags = values.getAsInteger(AttachmentColumns.FLAGS);
983                            }
984                            // Report all new attachments to the download service
985                            if (TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
986                                LogUtils.w(TAG, new Throwable(), "attachment with blank location");
987                            }
988                            mAttachmentService.attachmentChanged(getContext(), longId, flags);
989                            break;
990                    }
991                    break;
992                case QUICK_RESPONSE_ACCOUNT_ID:
993                    longId = Long.parseLong(uri.getPathSegments().get(2));
994                    values.put(QuickResponseColumns.ACCOUNT_KEY, longId);
995                    return insert(QuickResponse.CONTENT_URI, values);
996                case MAILBOX_ID:
997                    // This implies adding a message to a mailbox
998                    // Hmm, a problem here is that we can't link the account as well, so it must be
999                    // already in the values...
1000                    longId = Long.parseLong(uri.getPathSegments().get(1));
1001                    values.put(MessageColumns.MAILBOX_KEY, longId);
1002                    return insert(Message.CONTENT_URI, values); // Recurse
1003                case MESSAGE_ID:
1004                    // This implies adding an attachment to a message.
1005                    id = uri.getPathSegments().get(1);
1006                    longId = Long.parseLong(id);
1007                    values.put(AttachmentColumns.MESSAGE_KEY, longId);
1008                    return insert(Attachment.CONTENT_URI, values); // Recurse
1009                case ACCOUNT_ID:
1010                    // This implies adding a mailbox to an account.
1011                    longId = Long.parseLong(uri.getPathSegments().get(1));
1012                    values.put(MailboxColumns.ACCOUNT_KEY, longId);
1013                    return insert(Mailbox.CONTENT_URI, values); // Recurse
1014                case ATTACHMENTS_MESSAGE_ID:
1015                    longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values);
1016                    resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId);
1017                    break;
1018                default:
1019                    throw new IllegalArgumentException("Unknown URL " + uri);
1020            }
1021        } catch (SQLiteException e) {
1022            checkDatabases();
1023            throw e;
1024        }
1025
1026        // Notify all notifier cursors
1027        sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
1028
1029        // Notify all existing cursors.
1030        notifyUI(EmailContent.CONTENT_URI, null);
1031        return resultUri;
1032    }
1033
1034    @Override
1035    public boolean onCreate() {
1036        Context context = getContext();
1037        EmailContent.init(context);
1038        init(context);
1039        DebugUtils.init(context);
1040        // Do this last, so that EmailContent/EmailProvider are initialized
1041        setServicesEnabledAsync(context);
1042        reconcileAccountsAsync(context);
1043
1044        // Update widgets
1045        final Intent updateAllWidgetsIntent =
1046                new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED);
1047        updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true);
1048        updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type));
1049        context.sendBroadcast(updateAllWidgetsIntent);
1050
1051        // The combined account name changes on locale changes
1052        final Configuration oldConfiguration =
1053                new Configuration(context.getResources().getConfiguration());
1054        context.registerComponentCallbacks(new ComponentCallbacks() {
1055            @Override
1056            public void onConfigurationChanged(Configuration configuration) {
1057                int delta = oldConfiguration.updateFrom(configuration);
1058                if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) {
1059                    notifyUIAccount(COMBINED_ACCOUNT_ID);
1060                }
1061            }
1062
1063            @Override
1064            public void onLowMemory() {}
1065        });
1066
1067        MailPrefs.get(context).registerOnSharedPreferenceChangeListener(this);
1068
1069        return false;
1070    }
1071
1072    private static void init(final Context context) {
1073        // Synchronize on the matcher rather than the class object to minimize risk of contention
1074        // & deadlock.
1075        synchronized (sURIMatcher) {
1076            // We use the existence of this variable as indicative of whether this function has
1077            // already run.
1078            if (INTEGRITY_CHECK_URI != null) {
1079                return;
1080            }
1081            INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY +
1082                    "/integrityCheck");
1083            ACCOUNT_BACKUP_URI =
1084                    Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup");
1085            FOLDER_STATUS_URI =
1086                    Uri.parse("content://" + EmailContent.AUTHORITY + "/status");
1087            EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type);
1088
1089            final String uiNotificationAuthority =
1090                    EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications";
1091            UIPROVIDER_CONVERSATION_NOTIFIER =
1092                    Uri.parse("content://" + uiNotificationAuthority + "/uimessages");
1093            UIPROVIDER_FOLDER_NOTIFIER =
1094                    Uri.parse("content://" + uiNotificationAuthority + "/uifolder");
1095            UIPROVIDER_FOLDERLIST_NOTIFIER =
1096                    Uri.parse("content://" + uiNotificationAuthority + "/uifolders");
1097            UIPROVIDER_ACCOUNT_NOTIFIER =
1098                    Uri.parse("content://" + uiNotificationAuthority + "/uiaccount");
1099            // Not currently used
1100            /* UIPROVIDER_SETTINGS_NOTIFIER =
1101                    Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/
1102            UIPROVIDER_ATTACHMENT_NOTIFIER =
1103                    Uri.parse("content://" + uiNotificationAuthority + "/uiattachment");
1104            UIPROVIDER_ATTACHMENTS_NOTIFIER =
1105                    Uri.parse("content://" + uiNotificationAuthority + "/uiattachments");
1106            UIPROVIDER_ALL_ACCOUNTS_NOTIFIER =
1107                    Uri.parse("content://" + uiNotificationAuthority + "/uiaccts");
1108            UIPROVIDER_MESSAGE_NOTIFIER =
1109                    Uri.parse("content://" + uiNotificationAuthority + "/uimessage");
1110            UIPROVIDER_RECENT_FOLDERS_NOTIFIER =
1111                    Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders");
1112
1113            // All accounts
1114            sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT);
1115            // A specific account
1116            // insert into this URI causes a mailbox to be added to the account
1117            sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID);
1118            sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK);
1119
1120            // All mailboxes
1121            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX);
1122            // A specific mailbox
1123            // insert into this URI causes a message to be added to the mailbox
1124            // ** NOTE For now, the accountKey must be set manually in the values!
1125            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID);
1126            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#",
1127                    MAILBOX_NOTIFICATION);
1128            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#",
1129                    MAILBOX_MOST_RECENT_MESSAGE);
1130            sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT);
1131
1132            // All messages
1133            sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE);
1134            // A specific message
1135            // insert into this URI causes an attachment to be added to the message
1136            sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID);
1137
1138            // A specific attachment
1139            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT);
1140            // A specific attachment (the header information)
1141            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID);
1142            // The attachments of a specific message (query only) (insert & delete TBD)
1143            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#",
1144                    ATTACHMENTS_MESSAGE_ID);
1145            sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile",
1146                    ATTACHMENTS_CACHED_FILE_ACCESS);
1147
1148            // All mail bodies
1149            sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
1150            // A specific mail body
1151            sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
1152            // A specific HTML body part, for openFile
1153            sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML);
1154            // A specific text body part, for openFile
1155            sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT);
1156
1157            // All hostauth records
1158            sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
1159            // A specific hostauth
1160            sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID);
1161
1162            // All credential records
1163            sURIMatcher.addURI(EmailContent.AUTHORITY, "credential", CREDENTIAL);
1164            // A specific credential
1165            sURIMatcher.addURI(EmailContent.AUTHORITY, "credential/*", CREDENTIAL_ID);
1166
1167            /**
1168             * THIS URI HAS SPECIAL SEMANTICS
1169             * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK
1170             * TO A SERVER VIA A SYNC ADAPTER
1171             */
1172            sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID);
1173            sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION);
1174
1175            sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE);
1176            sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH,
1177                    MESSAGE_STATE_CHANGE);
1178
1179            /**
1180             * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY
1181             * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI
1182             * BY THE UI APPLICATION
1183             */
1184            // All deleted messages
1185            sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE);
1186            // A specific deleted message
1187            sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID);
1188
1189            // All updated messages
1190            sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE);
1191            // A specific updated message
1192            sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID);
1193
1194            sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY);
1195            sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID);
1196
1197            // All quick responses
1198            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE);
1199            // A specific quick response
1200            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID);
1201            // All quick responses associated with a particular account id
1202            sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#",
1203                    QUICK_RESPONSE_ACCOUNT_ID);
1204
1205            sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS);
1206            sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS);
1207            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS);
1208            sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS);
1209            sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES);
1210            sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE);
1211            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO);
1212            sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH);
1213            // We listen to everything trailing uifolder/ since there might be an appVersion
1214            // as in Utils.appendVersionQueryParameter().
1215            sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER);
1216            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiinbox/#", UI_INBOX);
1217            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT);
1218            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS);
1219            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiacctsettings", UI_ACCTSETTINGS);
1220            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS);
1221            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT);
1222            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachmentbycid/#/*",
1223                    UI_ATTACHMENT_BY_CID);
1224            sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH);
1225            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA);
1226            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE);
1227            sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION);
1228            sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS);
1229            sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
1230                    UI_DEFAULT_RECENT_FOLDERS);
1231            sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#",
1232                    ACCOUNT_PICK_TRASH_FOLDER);
1233            sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#",
1234                    ACCOUNT_PICK_SENT_FOLDER);
1235            sURIMatcher.addURI(EmailContent.AUTHORITY, "uipurgefolder/#", UI_PURGE_FOLDER);
1236        }
1237    }
1238
1239    /**
1240     * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
1241     * always be in sync (i.e. there are two database or NO databases).  This code will delete
1242     * any "orphan" database, so that both will be created together.  Note that an "orphan" database
1243     * will exist after either of the individual databases is deleted due to data corruption.
1244     */
1245    public void checkDatabases() {
1246        synchronized (sDatabaseLock) {
1247            // Uncache the databases
1248            if (mDatabase != null) {
1249                mDatabase = null;
1250            }
1251            if (mBodyDatabase != null) {
1252                mBodyDatabase = null;
1253            }
1254            // Look for orphans, and delete as necessary; these must always be in sync
1255            final File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
1256            final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
1257
1258            // TODO Make sure attachments are deleted
1259            if (databaseFile.exists() && !bodyFile.exists()) {
1260                LogUtils.w(TAG, "Deleting orphaned EmailProvider database...");
1261                getContext().deleteDatabase(DATABASE_NAME);
1262            } else if (bodyFile.exists() && !databaseFile.exists()) {
1263                LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database...");
1264                getContext().deleteDatabase(BODY_DATABASE_NAME);
1265            }
1266        }
1267    }
1268
1269    @Override
1270    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1271            String sortOrder) {
1272        Cursor c = null;
1273        int match;
1274        try {
1275            match = findMatch(uri, "query");
1276        } catch (IllegalArgumentException e) {
1277            String uriString = uri.toString();
1278            // If we were passed an illegal uri, see if it ends in /-1
1279            // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor
1280            if (uriString != null && uriString.endsWith("/-1")) {
1281                uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0");
1282                match = findMatch(uri, "query");
1283                switch (match) {
1284                    case BODY_ID:
1285                    case MESSAGE_ID:
1286                    case DELETED_MESSAGE_ID:
1287                    case UPDATED_MESSAGE_ID:
1288                    case ATTACHMENT_ID:
1289                    case MAILBOX_ID:
1290                    case ACCOUNT_ID:
1291                    case HOSTAUTH_ID:
1292                    case CREDENTIAL_ID:
1293                    case POLICY_ID:
1294                        return new MatrixCursorWithCachedColumns(projection, 0);
1295                }
1296            }
1297            throw e;
1298        }
1299        Context context = getContext();
1300        // See the comment at delete(), above
1301        SQLiteDatabase db = getDatabase(context);
1302        int table = match >> BASE_SHIFT;
1303        String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT);
1304        String id;
1305
1306        String tableName = TABLE_NAMES.valueAt(table);
1307
1308        try {
1309            switch (match) {
1310                // First, dispatch queries from UnifiedEmail
1311                case UI_SEARCH:
1312                    c = uiSearch(uri, projection);
1313                    return c;
1314                case UI_ACCTS:
1315                    final String suppressParam =
1316                            uri.getQueryParameter(EmailContent.SUPPRESS_COMBINED_ACCOUNT_PARAM);
1317                    final boolean suppressCombined =
1318                            suppressParam != null && Boolean.parseBoolean(suppressParam);
1319                    c = uiAccounts(projection, suppressCombined);
1320                    return c;
1321                case UI_UNDO:
1322                    return uiUndo(projection);
1323                case UI_SUBFOLDERS:
1324                case UI_MESSAGES:
1325                case UI_MESSAGE:
1326                case UI_FOLDER:
1327                case UI_INBOX:
1328                case UI_ACCOUNT:
1329                case UI_ATTACHMENT:
1330                case UI_ATTACHMENTS:
1331                case UI_ATTACHMENT_BY_CID:
1332                case UI_CONVERSATION:
1333                case UI_RECENT_FOLDERS:
1334                case UI_FULL_FOLDERS:
1335                case UI_ALL_FOLDERS:
1336                    // For now, we don't allow selection criteria within these queries
1337                    if (selection != null || selectionArgs != null) {
1338                        throw new IllegalArgumentException("UI queries can't have selection/args");
1339                    }
1340
1341                    final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER);
1342                    final boolean unseenOnly =
1343                            seenParam != null && Boolean.FALSE.toString().equals(seenParam);
1344
1345                    c = uiQuery(match, uri, projection, unseenOnly);
1346                    return c;
1347                case UI_FOLDERS:
1348                    c = uiFolders(uri, projection);
1349                    return c;
1350                case UI_FOLDER_LOAD_MORE:
1351                    c = uiFolderLoadMore(getMailbox(uri));
1352                    return c;
1353                case UI_FOLDER_REFRESH:
1354                    c = uiFolderRefresh(getMailbox(uri), 0);
1355                    return c;
1356                case MAILBOX_NOTIFICATION:
1357                    c = notificationQuery(uri);
1358                    return c;
1359                case MAILBOX_MOST_RECENT_MESSAGE:
1360                    c = mostRecentMessageQuery(uri);
1361                    return c;
1362                case MAILBOX_MESSAGE_COUNT:
1363                    c = getMailboxMessageCount(uri);
1364                    return c;
1365                case MESSAGE_MOVE:
1366                    return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs,
1367                            null, null, sortOrder, limit);
1368                case MESSAGE_STATE_CHANGE:
1369                    return db.query(MessageStateChange.TABLE_NAME, projection, selection,
1370                            selectionArgs, null, null, sortOrder, limit);
1371                case MESSAGE:
1372                case UPDATED_MESSAGE:
1373                case DELETED_MESSAGE:
1374                case ATTACHMENT:
1375                case MAILBOX:
1376                case ACCOUNT:
1377                case HOSTAUTH:
1378                case CREDENTIAL:
1379                case POLICY:
1380                    c = db.query(tableName, projection,
1381                            selection, selectionArgs, null, null, sortOrder, limit);
1382                    break;
1383                case QUICK_RESPONSE:
1384                    c = uiQuickResponse(projection);
1385                    break;
1386                case BODY:
1387                case BODY_ID: {
1388                    final ProjectionMap map = new ProjectionMap.Builder()
1389                            .addAll(projection)
1390                            .build();
1391                    if (map.containsKey(BodyColumns.HTML_CONTENT) ||
1392                            map.containsKey(BodyColumns.TEXT_CONTENT)) {
1393                        throw new IllegalArgumentException(
1394                                "Body content cannot be returned in the cursor");
1395                    }
1396
1397                    final ContentValues cv = new ContentValues(2);
1398                    cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml",
1399                            BodyColumns.MESSAGE_KEY));
1400                    cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText",
1401                            BodyColumns.MESSAGE_KEY));
1402
1403                    final StringBuilder sb = genSelect(map, projection, cv);
1404                    sb.append(" FROM ").append(Body.TABLE_NAME);
1405                    if (match == BODY_ID) {
1406                        id = uri.getPathSegments().get(1);
1407                        sb.append(" WHERE ").append(whereWithId(id, selection));
1408                    } else if (!TextUtils.isEmpty(selection)) {
1409                        sb.append(" WHERE ").append(selection);
1410                    }
1411                    if (!TextUtils.isEmpty(sortOrder)) {
1412                        sb.append(" ORDER BY ").append(sortOrder);
1413                    }
1414                    if (!TextUtils.isEmpty(limit)) {
1415                        sb.append(" LIMIT ").append(limit);
1416                    }
1417                    c = db.rawQuery(sb.toString(), selectionArgs);
1418                    break;
1419                }
1420                case MESSAGE_ID:
1421                case DELETED_MESSAGE_ID:
1422                case UPDATED_MESSAGE_ID:
1423                case ATTACHMENT_ID:
1424                case MAILBOX_ID:
1425                case HOSTAUTH_ID:
1426                case CREDENTIAL_ID:
1427                case POLICY_ID:
1428                    id = uri.getPathSegments().get(1);
1429                    c = db.query(tableName, projection, whereWithId(id, selection),
1430                            selectionArgs, null, null, sortOrder, limit);
1431                    break;
1432                case ACCOUNT_ID:
1433                    id = uri.getPathSegments().get(1);
1434                    // There seems to be an issue with smart forwarding sometimes including the
1435                    // quoted text from the wrong message. For now, we just disable it.
1436                    final String[] alternateProjection = new String[projection.length];
1437                    for (int i = 0; i < projection.length; i++) {
1438                        String column = projection[i];
1439                        if (TextUtils.equals(column, AccountColumns.FLAGS)) {
1440                            alternateProjection[i] = AccountColumns.FLAGS + " & ~" +
1441                                    Account.FLAGS_SUPPORTS_SMART_FORWARD + " AS " +
1442                                    AccountColumns.FLAGS;
1443                        } else {
1444                            alternateProjection[i] = projection[i];
1445                        }
1446                    }
1447
1448                    c = db.query(tableName, alternateProjection, whereWithId(id, selection),
1449                            selectionArgs, null, null, sortOrder, limit);
1450                    break;
1451                case QUICK_RESPONSE_ID:
1452                    id = uri.getPathSegments().get(1);
1453                    c = uiQuickResponseId(projection, id);
1454                    break;
1455                case ATTACHMENTS_MESSAGE_ID:
1456                    // All attachments for the given message
1457                    id = uri.getPathSegments().get(2);
1458                    c = db.query(Attachment.TABLE_NAME, projection,
1459                            whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection),
1460                            selectionArgs, null, null, sortOrder, limit);
1461                    break;
1462                case QUICK_RESPONSE_ACCOUNT_ID:
1463                    // All quick responses for the given account
1464                    id = uri.getPathSegments().get(2);
1465                    c = uiQuickResponseAccount(projection, id);
1466                    break;
1467                case ATTACHMENTS_CACHED_FILE_ACCESS:
1468                    if (projection == null) {
1469                        projection =
1470                                new String[] {
1471                                        AttachmentUtilities.Columns._ID,
1472                                        AttachmentUtilities.Columns.DATA,
1473                                };
1474                    }
1475                    // Map the columns of our attachment table to the columns defined in
1476                    // AttachmentUtils. These are a superset of OpenableColumns.
1477                    // This mirrors similar code in AttachmentProvider.
1478                    c = db.query(Attachment.TABLE_NAME,
1479                            CACHED_FILE_QUERY_PROJECTION, AttachmentColumns.CACHED_FILE + "=?",
1480                            new String[]{uri.toString()}, null, null, null, null);
1481                    try {
1482                        if (c.getCount() > 1) {
1483                            LogUtils.e(TAG, "multiple results querying CACHED_FILE_ACCESS %s", uri);
1484                        }
1485                        if (c != null && c.moveToFirst()) {
1486                            MatrixCursor ret = new MatrixCursorWithCachedColumns(projection);
1487                            Object[] values = new Object[projection.length];
1488                            for (int i = 0, count = projection.length; i < count; i++) {
1489                                String column = projection[i];
1490                                if (AttachmentUtilities.Columns._ID.equals(column)) {
1491                                    values[i] = c.getLong(
1492                                            c.getColumnIndexOrThrow(AttachmentColumns._ID));
1493                                }
1494                                else if (AttachmentUtilities.Columns.DATA.equals(column)) {
1495                                    values[i] = c.getString(
1496                                            c.getColumnIndexOrThrow(AttachmentColumns.CONTENT_URI));
1497                                }
1498                                else if (AttachmentUtilities.Columns.DISPLAY_NAME.equals(column)) {
1499                                    values[i] = c.getString(
1500                                            c.getColumnIndexOrThrow(AttachmentColumns.FILENAME));
1501                                }
1502                                else if (AttachmentUtilities.Columns.SIZE.equals(column)) {
1503                                    values[i] = c.getInt(
1504                                            c.getColumnIndexOrThrow(AttachmentColumns.SIZE));
1505                                } else {
1506                                    LogUtils.e(TAG,
1507                                            "unexpected column %s requested for CACHED_FILE",
1508                                            column);
1509                                }
1510                            }
1511                            ret.addRow(values);
1512                            return ret;
1513                        }
1514                    } finally {
1515                        if (c !=  null) {
1516                            c.close();
1517                        }
1518                    }
1519                    return null;
1520                default:
1521                    throw new IllegalArgumentException("Unknown URI " + uri);
1522            }
1523        } catch (SQLiteException e) {
1524            checkDatabases();
1525            throw e;
1526        } catch (RuntimeException e) {
1527            checkDatabases();
1528            e.printStackTrace();
1529            throw e;
1530        } finally {
1531            if (c == null) {
1532                // This should never happen, but let's be sure to log it...
1533                // TODO: There are actually cases where c == null is expected, for example
1534                // UI_FOLDER_LOAD_MORE.
1535                // Demoting this to a warning for now until we figure out what to do with it.
1536                LogUtils.w(TAG, "Query returning null for uri: %s selection: %s", uri, selection);
1537            }
1538        }
1539
1540        if ((c != null) && !isTemporary()) {
1541            c.setNotificationUri(getContext().getContentResolver(), uri);
1542        }
1543        return c;
1544    }
1545
1546    private static String whereWithId(String id, String selection) {
1547        StringBuilder sb = new StringBuilder(256);
1548        sb.append("_id=");
1549        sb.append(id);
1550        if (selection != null) {
1551            sb.append(" AND (");
1552            sb.append(selection);
1553            sb.append(')');
1554        }
1555        return sb.toString();
1556    }
1557
1558    /**
1559     * Combine a locally-generated selection with a user-provided selection
1560     *
1561     * This introduces risk that the local selection might insert incorrect chars
1562     * into the SQL, so use caution.
1563     *
1564     * @param where locally-generated selection, must not be null
1565     * @param selection user-provided selection, may be null
1566     * @return a single selection string
1567     */
1568    private static String whereWith(String where, String selection) {
1569        if (selection == null) {
1570            return where;
1571        }
1572        return where + " AND (" + selection + ")";
1573    }
1574
1575    /**
1576     * Restore a HostAuth from a database, given its unique id
1577     * @param db the database
1578     * @param id the unique id (_id) of the row
1579     * @return a fully populated HostAuth or null if the row does not exist
1580     */
1581    private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) {
1582        Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION,
1583                HostAuthColumns._ID + "=?", new String[] {Long.toString(id)}, null, null, null);
1584        try {
1585            if (c.moveToFirst()) {
1586                HostAuth hostAuth = new HostAuth();
1587                hostAuth.restore(c);
1588                return hostAuth;
1589            }
1590            return null;
1591        } finally {
1592            c.close();
1593        }
1594    }
1595
1596    /**
1597     * Copy the Account and HostAuth tables from one database to another
1598     * @param fromDatabase the source database
1599     * @param toDatabase the destination database
1600     * @return the number of accounts copied, or -1 if an error occurred
1601     */
1602    private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) {
1603        if (fromDatabase == null || toDatabase == null) return -1;
1604
1605        // Lock both databases; for the "from" database, we don't want anyone changing it from
1606        // under us; for the "to" database, we want to make the operation atomic
1607        int copyCount = 0;
1608        fromDatabase.beginTransaction();
1609        try {
1610            toDatabase.beginTransaction();
1611            try {
1612                // Delete anything hanging around here
1613                toDatabase.delete(Account.TABLE_NAME, null, null);
1614                toDatabase.delete(HostAuth.TABLE_NAME, null, null);
1615
1616                // Get our account cursor
1617                Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
1618                        null, null, null, null, null);
1619                if (c == null) return 0;
1620                LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount());
1621                try {
1622                    // Loop through accounts, copying them and associated host auth's
1623                    while (c.moveToNext()) {
1624                        Account account = new Account();
1625                        account.restore(c);
1626
1627                        // Clear security sync key and sync key, as these were specific to the
1628                        // state of the account, and we've reset that...
1629                        // Clear policy key so that we can re-establish policies from the server
1630                        // TODO This is pretty EAS specific, but there's a lot of that around
1631                        account.mSecuritySyncKey = null;
1632                        account.mSyncKey = null;
1633                        account.mPolicyKey = 0;
1634
1635                        // Copy host auth's and update foreign keys
1636                        HostAuth hostAuth = restoreHostAuth(fromDatabase,
1637                                account.mHostAuthKeyRecv);
1638
1639                        // The account might have gone away, though very unlikely
1640                        if (hostAuth == null) continue;
1641                        account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null,
1642                                hostAuth.toContentValues());
1643
1644                        // EAS accounts have no send HostAuth
1645                        if (account.mHostAuthKeySend > 0) {
1646                            hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend);
1647                            // Belt and suspenders; I can't imagine that this is possible,
1648                            // since we checked the validity of the account above, and the
1649                            // database is now locked
1650                            if (hostAuth == null) continue;
1651                            account.mHostAuthKeySend = toDatabase.insert(
1652                                    HostAuth.TABLE_NAME, null, hostAuth.toContentValues());
1653                        }
1654
1655                        // Now, create the account in the "to" database
1656                        toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues());
1657                        copyCount++;
1658                    }
1659                } finally {
1660                    c.close();
1661                }
1662
1663                // Say it's ok to commit
1664                toDatabase.setTransactionSuccessful();
1665            } finally {
1666                toDatabase.endTransaction();
1667            }
1668        } catch (SQLiteException ex) {
1669            LogUtils.w(TAG, "Exception while copying account tables", ex);
1670            copyCount = -1;
1671        } finally {
1672            fromDatabase.endTransaction();
1673        }
1674        return copyCount;
1675    }
1676
1677    /**
1678     * Backup account data, returning the number of accounts backed up
1679     */
1680    private static int backupAccounts(final Context context, final SQLiteDatabase db) {
1681        final AccountManager am = AccountManager.get(context);
1682        final Cursor accountCursor = db.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION,
1683                null, null, null, null, null);
1684        int updatedCount = 0;
1685        try {
1686            while (accountCursor.moveToNext()) {
1687                final Account account = new Account();
1688                account.restore(accountCursor);
1689                EmailServiceInfo serviceInfo =
1690                        EmailServiceUtils.getServiceInfo(context, account.getProtocol(context));
1691                if (serviceInfo == null) {
1692                    LogUtils.d(LogUtils.TAG, "Could not find service info for account");
1693                    continue;
1694                }
1695                final String jsonString = account.toJsonString(context);
1696                final android.accounts.Account amAccount =
1697                        account.getAccountManagerAccount(serviceInfo.accountType);
1698                am.setUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG, jsonString);
1699                updatedCount++;
1700            }
1701        } finally {
1702            accountCursor.close();
1703        }
1704        return updatedCount;
1705    }
1706
1707    /**
1708     * Restore account data, returning the number of accounts restored
1709     */
1710    private static int restoreAccounts(final Context context) {
1711        final Collection<EmailServiceInfo> infos = EmailServiceUtils.getServiceInfoList(context);
1712        // Find all possible account types
1713        final Set<String> accountTypes = new HashSet<String>(3);
1714        for (final EmailServiceInfo info : infos) {
1715            if (!TextUtils.isEmpty(info.accountType)) {
1716                // accountType will be empty for the gmail stub entry
1717                accountTypes.add(info.accountType);
1718            }
1719        }
1720        // Find all accounts we own
1721        final List<android.accounts.Account> amAccounts = new ArrayList<android.accounts.Account>();
1722        final AccountManager am = AccountManager.get(context);
1723        for (final String accountType : accountTypes) {
1724            amAccounts.addAll(Arrays.asList(am.getAccountsByType(accountType)));
1725        }
1726        // Try to restore them from saved JSON
1727        int restoredCount = 0;
1728        for (final android.accounts.Account amAccount : amAccounts) {
1729            final String jsonString = am.getUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG);
1730            if (TextUtils.isEmpty(jsonString)) {
1731                continue;
1732            }
1733            final Account account = Account.fromJsonString(jsonString);
1734            if (account != null) {
1735                AccountSettingsUtils.commitSettings(context, account);
1736                final Bundle extras = new Bundle(3);
1737                extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
1738                extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
1739                extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
1740                ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
1741                restoredCount++;
1742            }
1743        }
1744        return restoredCount;
1745    }
1746
1747    private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s ("
1748            + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + ","
1749            + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ",";
1750
1751    private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, "
1752            + "(select " + MessageColumns.SERVER_ID + " from " +
1753                    Message.TABLE_NAME + " where _id=%s),"
1754            + "(select " + MessageColumns.ACCOUNT_KEY + " from " +
1755                    Message.TABLE_NAME + " where _id=%s),"
1756            + MessageMove.STATUS_NONE_STRING + ",";
1757
1758    /**
1759     * Formatting string to generate the SQL statement for inserting into MessageMove.
1760     * The formatting parameters are:
1761     * table name, message id x 4, destination folder id, message id, destination folder id.
1762     * Duplications are needed for sub-selects.
1763     */
1764    private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
1765            + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + ","
1766            + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID
1767            + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
1768            + "(select " + MessageColumns.MAILBOX_KEY +
1769                    " from " + Message.TABLE_NAME + " where _id=%s)," + "%d,"
1770            + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select "
1771            + MessageColumns.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)),"
1772            + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))";
1773
1774    /**
1775     * Insert a row into the MessageMove table when that message is moved.
1776     * @param db The {@link SQLiteDatabase}.
1777     * @param messageId The id of the message being moved.
1778     * @param dstFolderKey The folder to which the message is being moved.
1779     */
1780    private void addToMessageMove(final SQLiteDatabase db, final String messageId,
1781            final long dstFolderKey) {
1782        db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME,
1783                messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey));
1784    }
1785
1786    /**
1787     * Formatting string to generate the SQL statement for inserting into MessageStateChange.
1788     * The formatting parameters are:
1789     * table name, message id x 4, new flag read, message id, new flag favorite.
1790     * Duplications are needed for sub-selects.
1791     */
1792    private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX
1793            + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + ","
1794            + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE
1795            + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX
1796            + "(select " + MessageColumns.FLAG_READ +
1797            " from " + Message.TABLE_NAME + " where _id=%s)," + "%d,"
1798            + "(select " + MessageColumns.FLAG_FAVORITE +
1799            " from " + Message.TABLE_NAME + " where _id=%s)," + "%d)";
1800
1801    private void addToMessageStateChange(final SQLiteDatabase db, final String messageId,
1802            final int newFlagRead, final int newFlagFavorite) {
1803        db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT,
1804                MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId,
1805                newFlagRead, messageId, newFlagFavorite));
1806    }
1807
1808    // select count(*) from (select count(*) as dupes from Mailbox where accountKey=?
1809    // group by serverId) where dupes > 1;
1810    private static final String ACCOUNT_INTEGRITY_SQL =
1811            "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME +
1812            " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1";
1813
1814
1815    // Query to get the protocol for a message. Temporary to switch between new and old upsync
1816    // behavior; should go away when IMAP gets converted.
1817    private static final String GET_MESSAGE_DETAILS = "SELECT"
1818            + " h." + HostAuthColumns.PROTOCOL + ","
1819            + " m." + MessageColumns.MAILBOX_KEY + ","
1820            + " a." + AccountColumns._ID
1821            + " FROM " + Message.TABLE_NAME + " AS m"
1822            + " INNER JOIN " + Account.TABLE_NAME + " AS a"
1823            + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns._ID
1824            + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
1825            + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID
1826            + " WHERE m." + MessageColumns._ID + "=?";
1827    private static final int INDEX_PROTOCOL = 0;
1828    private static final int INDEX_MAILBOX_KEY = 1;
1829    private static final int INDEX_ACCOUNT_KEY = 2;
1830
1831    /**
1832     * Query to get the protocol and email address for an account. Note that this uses
1833     * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns.
1834     */
1835    private static final String GET_ACCOUNT_DETAILS = "SELECT"
1836            + " h." + HostAuthColumns.PROTOCOL + ","
1837            + " a." + AccountColumns.EMAIL_ADDRESS + ","
1838            + " a." + AccountColumns.SYNC_KEY
1839            + " FROM " + Account.TABLE_NAME + " AS a"
1840            + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h"
1841            + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID
1842            + " WHERE a." + AccountColumns._ID + "=?";
1843    private static final int INDEX_EMAIL_ADDRESS = 1;
1844    private static final int INDEX_SYNC_KEY = 2;
1845
1846    /**
1847     * Restart push if we need it (currently only for Exchange accounts).
1848     * @param context A {@link Context}.
1849     * @param db The {@link SQLiteDatabase}.
1850     * @param id The id of the thing we're looking for.
1851     * @return Whether or not we sent a request to restart the push.
1852     */
1853    private static boolean restartPush(final Context context, final SQLiteDatabase db,
1854            final String id) {
1855        final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id});
1856        if (c != null) {
1857            try {
1858                if (c.moveToFirst()) {
1859                    final String protocol = c.getString(INDEX_PROTOCOL);
1860                    // Only restart push for EAS accounts that have completed initial sync.
1861                    if (context.getString(R.string.protocol_eas).equals(protocol) &&
1862                            !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) {
1863                        final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
1864                        final android.accounts.Account account =
1865                                getAccountManagerAccount(context, emailAddress, protocol);
1866                        if (account != null) {
1867                            restartPush(account);
1868                            return true;
1869                        }
1870                    }
1871                }
1872            } finally {
1873                c.close();
1874            }
1875        }
1876        return false;
1877    }
1878
1879    /**
1880     * Restart push if a mailbox's settings change in a way that requires it.
1881     * @param context A {@link Context}.
1882     * @param db The {@link SQLiteDatabase}.
1883     * @param values The {@link ContentValues} that were updated for the mailbox.
1884     * @param accountId The id of the account for this mailbox.
1885     * @return Whether or not the push was restarted.
1886     */
1887    private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db,
1888            final ContentValues values, final String accountId) {
1889        if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) ||
1890                values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
1891            return restartPush(context, db, accountId);
1892        }
1893        return false;
1894    }
1895
1896    /**
1897     * Restart push if an account's settings change in a way that requires it.
1898     * @param context A {@link Context}.
1899     * @param db The {@link SQLiteDatabase}.
1900     * @param values The {@link ContentValues} that were updated for the account.
1901     * @param accountId The id of the account.
1902     * @return Whether or not the push was restarted.
1903     */
1904    private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db,
1905            final ContentValues values, final String accountId) {
1906        if (values.containsKey(AccountColumns.SYNC_LOOKBACK) ||
1907                values.containsKey(AccountColumns.SYNC_INTERVAL)) {
1908            return restartPush(context, db, accountId);
1909        }
1910        return false;
1911    }
1912
1913    @Override
1914    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1915        LogUtils.d(TAG, "Update: " + uri);
1916        // Handle this special case the fastest possible way
1917        if (INTEGRITY_CHECK_URI.equals(uri)) {
1918            checkDatabases();
1919            return 0;
1920        } else if (ACCOUNT_BACKUP_URI.equals(uri)) {
1921            return backupAccounts(getContext(), getDatabase(getContext()));
1922        }
1923
1924        // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID)
1925        Uri notificationUri = EmailContent.CONTENT_URI;
1926
1927        final int match = findMatch(uri, "update");
1928        final Context context = getContext();
1929        // See the comment at delete(), above
1930        final SQLiteDatabase db = getDatabase(context);
1931        final int table = match >> BASE_SHIFT;
1932        int result;
1933
1934        // We do NOT allow setting of unreadCount/messageCount via the provider
1935        // These columns are maintained via triggers
1936        if (match == MAILBOX_ID || match == MAILBOX) {
1937            values.remove(MailboxColumns.UNREAD_COUNT);
1938            values.remove(MailboxColumns.MESSAGE_COUNT);
1939        }
1940
1941        final String tableName = TABLE_NAMES.valueAt(table);
1942        String id = "0";
1943
1944        try {
1945            switch (match) {
1946                case ACCOUNT_PICK_TRASH_FOLDER:
1947                    return pickTrashFolder(uri);
1948                case ACCOUNT_PICK_SENT_FOLDER:
1949                    return pickSentFolder(uri);
1950                case UI_ACCTSETTINGS:
1951                    return uiUpdateSettings(context, values);
1952                case UI_FOLDER:
1953                    return uiUpdateFolder(context, uri, values);
1954                case UI_RECENT_FOLDERS:
1955                    return uiUpdateRecentFolders(uri, values);
1956                case UI_DEFAULT_RECENT_FOLDERS:
1957                    return uiPopulateRecentFolders(uri);
1958                case UI_ATTACHMENT:
1959                    return uiUpdateAttachment(uri, values);
1960                case UI_MESSAGE:
1961                    return uiUpdateMessage(uri, values);
1962                case ACCOUNT_CHECK:
1963                    id = uri.getLastPathSegment();
1964                    // With any error, return 1 (a failure)
1965                    int res = 1;
1966                    Cursor ic = null;
1967                    try {
1968                        ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id});
1969                        if (ic.moveToFirst()) {
1970                            res = ic.getInt(0);
1971                        }
1972                    } finally {
1973                        if (ic != null) {
1974                            ic.close();
1975                        }
1976                    }
1977                    // Count of duplicated mailboxes
1978                    return res;
1979                case MESSAGE_SELECTION:
1980                    Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection,
1981                            selectionArgs, null, null, null);
1982                    try {
1983                        if (findCursor.moveToFirst()) {
1984                            return update(ContentUris.withAppendedId(
1985                                    Message.CONTENT_URI,
1986                                    findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)),
1987                                    values, null, null);
1988                        } else {
1989                            return 0;
1990                        }
1991                    } finally {
1992                        findCursor.close();
1993                    }
1994                case SYNCED_MESSAGE_ID:
1995                case UPDATED_MESSAGE_ID:
1996                case MESSAGE_ID:
1997                case ATTACHMENT_ID:
1998                case MAILBOX_ID:
1999                case ACCOUNT_ID:
2000                case HOSTAUTH_ID:
2001                case CREDENTIAL_ID:
2002                case QUICK_RESPONSE_ID:
2003                case POLICY_ID:
2004                    id = uri.getPathSegments().get(1);
2005                    if (match == SYNCED_MESSAGE_ID) {
2006                        // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well.
2007                        boolean isEas = false;
2008                        long mailboxId = -1;
2009                        long accountId = -1;
2010                        final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id});
2011                        if (c != null) {
2012                            try {
2013                                if (c.moveToFirst()) {
2014                                    final String protocol = c.getString(INDEX_PROTOCOL);
2015                                    isEas = context.getString(R.string.protocol_eas)
2016                                            .equals(protocol);
2017                                    mailboxId = c.getLong(INDEX_MAILBOX_KEY);
2018                                    accountId = c.getLong(INDEX_ACCOUNT_KEY);
2019                                }
2020                            } finally {
2021                                c.close();
2022                            }
2023                        }
2024
2025                        if (isEas) {
2026                            // EAS uses the new upsync classes.
2027                            Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY);
2028                            if (dstFolderId != null) {
2029                                addToMessageMove(db, id, dstFolderId);
2030                            }
2031                            Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ);
2032                            Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE);
2033                            int flagReadValue = (flagRead != null) ?
2034                                    flagRead : MessageStateChange.VALUE_UNCHANGED;
2035                            int flagFavoriteValue = (flagFavorite != null) ?
2036                                    flagFavorite : MessageStateChange.VALUE_UNCHANGED;
2037                            if (flagRead != null || flagFavorite != null) {
2038                                addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue);
2039                            }
2040
2041                            // Request a sync for the messages mailbox so the update will upsync.
2042                            // This is normally done with ContentResolver.notifyUpdate() but doesn't
2043                            // work for Exchange because the Sync Adapter is declared as
2044                            // android:supportsUploading="false". Changing it to true is not trivial
2045                            // because that would require us to protect all calls to notifyUpdate()
2046                            // with syncToServer=false except in cases where we actually want to
2047                            // upsync.
2048                            // TODO: Look into making Exchange Sync Adapter supportsUploading=true
2049                            // Since we can't use the Sync Manager "delayed-sync" feature which
2050                            // applies only to UPLOAD syncs, we need to do this ourselves. The
2051                            // purpose of this is not to spam syncs when making frequent
2052                            // modifications.
2053                            final Handler handler = getDelayedSyncHandler();
2054                            final android.accounts.Account amAccount =
2055                                    getAccountManagerAccount(accountId);
2056                            if (amAccount != null) {
2057                                final SyncRequestMessage request = new SyncRequestMessage(
2058                                        uri.getAuthority(), amAccount, mailboxId);
2059                                synchronized (mDelayedSyncRequests) {
2060                                    if (!mDelayedSyncRequests.contains(request)) {
2061                                        mDelayedSyncRequests.add(request);
2062                                        final android.os.Message message =
2063                                                handler.obtainMessage(0, request);
2064                                        handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS);
2065                                    }
2066                                }
2067                            } else {
2068                                LogUtils.d(TAG,
2069                                        "Attempted to start delayed sync for invalid account %d",
2070                                        accountId);
2071                            }
2072                        } else {
2073                            // Old way of doing upsync.
2074                            // For synced messages, first copy the old message to the updated table
2075                            // Note the insert or ignore semantics, guaranteeing that only the first
2076                            // update will be reflected in the updated message table; therefore this
2077                            // row will always have the "original" data
2078                            db.execSQL(UPDATED_MESSAGE_INSERT + id);
2079                        }
2080                    } else if (match == MESSAGE_ID) {
2081                        db.execSQL(UPDATED_MESSAGE_DELETE + id);
2082                    }
2083                    result = db.update(tableName, values, whereWithId(id, selection),
2084                            selectionArgs);
2085                    if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) {
2086                        handleMessageUpdateNotifications(uri, id, values);
2087                    } else if (match == ATTACHMENT_ID) {
2088                        long attId = Integer.parseInt(id);
2089                        if (values.containsKey(AttachmentColumns.FLAGS)) {
2090                            int flags = values.getAsInteger(AttachmentColumns.FLAGS);
2091                            mAttachmentService.attachmentChanged(context, attId, flags);
2092                        }
2093                        // Notify UI if necessary; there are only two columns we can change that
2094                        // would be worth a notification
2095                        if (values.containsKey(AttachmentColumns.UI_STATE) ||
2096                                values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) {
2097                            // Notify on individual attachment
2098                            notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id);
2099                            Attachment att = Attachment.restoreAttachmentWithId(context, attId);
2100                            if (att != null) {
2101                                // And on owning Message
2102                                notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey);
2103                            }
2104                        }
2105                    } else if (match == MAILBOX_ID) {
2106                        final long accountId = Mailbox.getAccountIdForMailbox(context, id);
2107                        notifyUIFolder(id, accountId);
2108                        restartPushForMailbox(context, db, values, Long.toString(accountId));
2109                    } else if (match == ACCOUNT_ID) {
2110                        updateAccountSyncInterval(Long.parseLong(id), values);
2111                        // Notify individual account and "all accounts"
2112                        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id);
2113                        notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
2114                        restartPushForAccount(context, db, values, id);
2115                    }
2116                    break;
2117                case BODY_ID: {
2118                    final ContentValues updateValues = new ContentValues(values);
2119                    updateValues.remove(BodyColumns.HTML_CONTENT);
2120                    updateValues.remove(BodyColumns.TEXT_CONTENT);
2121
2122                    result = db.update(tableName, updateValues, whereWithId(id, selection),
2123                            selectionArgs);
2124
2125                    if (values.containsKey(BodyColumns.HTML_CONTENT) ||
2126                            values.containsKey(BodyColumns.TEXT_CONTENT)) {
2127                        final long messageId;
2128                        if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
2129                            messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
2130                        } else {
2131                            final long bodyId = Long.parseLong(id);
2132                            final SQLiteStatement sql = db.compileStatement(
2133                                    "select " + BodyColumns.MESSAGE_KEY +
2134                                            " from " + Body.TABLE_NAME +
2135                                            " where " + BodyColumns._ID + "=" + Long
2136                                            .toString(bodyId)
2137                            );
2138                            messageId = sql.simpleQueryForLong();
2139                        }
2140                        writeBodyFiles(context, messageId, values);
2141                    }
2142                    break;
2143                }
2144                case BODY: {
2145                    final ContentValues updateValues = new ContentValues(values);
2146                    updateValues.remove(BodyColumns.HTML_CONTENT);
2147                    updateValues.remove(BodyColumns.TEXT_CONTENT);
2148
2149                    result = db.update(tableName, updateValues, selection, selectionArgs);
2150
2151                    if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) {
2152                        // TODO: This is a hack. Notably, the selection equality test above
2153                        // is hokey at best.
2154                        LogUtils.i(TAG, "Body Update to non-existent row, morphing to insert");
2155                        final ContentValues insertValues = new ContentValues(values);
2156                        insertValues.put(BodyColumns.MESSAGE_KEY, selectionArgs[0]);
2157                        insert(Body.CONTENT_URI, insertValues);
2158                    } else {
2159                        // possibly need to write new body values
2160                        if (values.containsKey(BodyColumns.HTML_CONTENT) ||
2161                                values.containsKey(BodyColumns.TEXT_CONTENT)) {
2162                            final long messageIds[];
2163                            if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
2164                                messageIds = new long[] {values.getAsLong(BodyColumns.MESSAGE_KEY)};
2165                            } else if (values.containsKey(BodyColumns._ID)) {
2166                                final long bodyId = values.getAsLong(BodyColumns._ID);
2167                                final SQLiteStatement sql = db.compileStatement(
2168                                        "select " + BodyColumns.MESSAGE_KEY +
2169                                                " from " + Body.TABLE_NAME +
2170                                                " where " + BodyColumns._ID + "=" + Long
2171                                                .toString(bodyId)
2172                                );
2173                                messageIds = new long[] {sql.simpleQueryForLong()};
2174                            } else {
2175                                final String proj[] = {BodyColumns.MESSAGE_KEY};
2176                                final Cursor c = db.query(Body.TABLE_NAME, proj,
2177                                        selection, selectionArgs,
2178                                        null, null, null);
2179                                try {
2180                                    final int count = c.getCount();
2181                                    if (count == 0) {
2182                                        throw new IllegalStateException("Can't find body record");
2183                                    }
2184                                    messageIds = new long[count];
2185                                    int i = 0;
2186                                    while (c.moveToNext()) {
2187                                        messageIds[i++] = c.getLong(0);
2188                                    }
2189                                } finally {
2190                                    c.close();
2191                                }
2192                            }
2193                            // This is probably overkill
2194                            for (int i = 0; i < messageIds.length; i++) {
2195                                final long messageId = messageIds[i];
2196                                writeBodyFiles(context, messageId, values);
2197                            }
2198                        }
2199                    }
2200                    break;
2201                }
2202                case MESSAGE:
2203                    decodeEmailAddresses(values);
2204                case UPDATED_MESSAGE:
2205                case ATTACHMENT:
2206                case MAILBOX:
2207                case ACCOUNT:
2208                case HOSTAUTH:
2209                case CREDENTIAL:
2210                case POLICY:
2211                    if (match == ATTACHMENT) {
2212                        if (values.containsKey(AttachmentColumns.LOCATION) &&
2213                                TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
2214                            LogUtils.w(TAG, new Throwable(), "attachment with blank location");
2215                        }
2216                    }
2217                    result = db.update(tableName, values, selection, selectionArgs);
2218                    break;
2219                case MESSAGE_MOVE:
2220                    result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs);
2221                    break;
2222                case MESSAGE_STATE_CHANGE:
2223                    result = db.update(MessageStateChange.TABLE_NAME, values, selection,
2224                            selectionArgs);
2225                    break;
2226                default:
2227                    throw new IllegalArgumentException("Unknown URI " + uri);
2228            }
2229        } catch (SQLiteException e) {
2230            checkDatabases();
2231            throw e;
2232        }
2233
2234        // Notify all notifier cursors if some records where changed in the database
2235        if (result > 0) {
2236            sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
2237            notifyUI(notificationUri, null);
2238        }
2239        return result;
2240    }
2241
2242    private void updateSyncStatus(final Bundle extras) {
2243        final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID);
2244        final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE);
2245        final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id);
2246        notifyUI(uri, null);
2247        final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS;
2248        if (inProgress) {
2249            RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id);
2250        } else {
2251            final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT);
2252            final ContentValues values = new ContentValues();
2253            values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
2254            mDatabase.update(
2255                    Mailbox.TABLE_NAME,
2256                    values,
2257                    WHERE_ID,
2258                    new String[] { String.valueOf(id) });
2259        }
2260    }
2261
2262    @Override
2263    public Bundle call(String method, String arg, Bundle extras) {
2264        LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg);
2265
2266        // Handle queries for the device friendly name.
2267        // TODO: This should eventually be a device property, not defined by the app.
2268        if (TextUtils.equals(method, EmailContent.DEVICE_FRIENDLY_NAME)) {
2269            final Bundle bundle = new Bundle(1);
2270            // TODO: For now, just use the model name since we don't yet have a user-supplied name.
2271            bundle.putString(EmailContent.DEVICE_FRIENDLY_NAME, Build.MODEL);
2272            return bundle;
2273        }
2274
2275        // Handle sync status callbacks.
2276        if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) {
2277            updateSyncStatus(extras);
2278            return null;
2279        }
2280        if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) {
2281            fixParentKeys(getDatabase(getContext()));
2282            return null;
2283        }
2284
2285        // Handle send & save.
2286        final Uri accountUri = Uri.parse(arg);
2287        final long accountId = Long.parseLong(accountUri.getPathSegments().get(1));
2288
2289        Uri messageUri = null;
2290
2291        if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) {
2292            messageUri = uiSendDraftMessage(accountId, extras);
2293            Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId);
2294        } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) {
2295            messageUri = uiSaveDraftMessage(accountId, extras);
2296        } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) {
2297            LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method);
2298        } else {
2299            LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method);
2300        }
2301
2302        final Bundle result;
2303        if (messageUri != null) {
2304            result = new Bundle(1);
2305            result.putParcelable(UIProvider.MessageColumns.URI, messageUri);
2306        } else {
2307            result = null;
2308        }
2309
2310        return result;
2311    }
2312
2313    private static void deleteBodyFiles(final Context c, final long messageId)
2314            throws IllegalStateException {
2315        final ContentValues emptyValues = new ContentValues(2);
2316        emptyValues.putNull(BodyColumns.HTML_CONTENT);
2317        emptyValues.putNull(BodyColumns.TEXT_CONTENT);
2318        writeBodyFiles(c, messageId, emptyValues);
2319    }
2320
2321    /**
2322     * Writes message bodies to disk, read from a set of ContentValues
2323     *
2324     * @param c Context for finding files
2325     * @param messageId id of message to write body for
2326     * @param cv {@link ContentValues} containing {@link BodyColumns#HTML_CONTENT} and/or
2327     *           {@link BodyColumns#TEXT_CONTENT}. Inserting a null or empty value will delete the
2328     *           associated text or html body file
2329     * @throws IllegalStateException
2330     */
2331    private static void writeBodyFiles(final Context c, final long messageId,
2332            final ContentValues cv) throws IllegalStateException {
2333        if (cv.containsKey(BodyColumns.HTML_CONTENT)) {
2334            final String htmlContent = cv.getAsString(BodyColumns.HTML_CONTENT);
2335            try {
2336                writeBodyFile(c, messageId, "html", htmlContent);
2337            } catch (final IOException e) {
2338                throw new IllegalStateException("IOException while writing html body " +
2339                        "for message id " + Long.toString(messageId), e);
2340            }
2341        }
2342        if (cv.containsKey(BodyColumns.TEXT_CONTENT)) {
2343            final String textContent = cv.getAsString(BodyColumns.TEXT_CONTENT);
2344            try {
2345                writeBodyFile(c, messageId, "txt", textContent);
2346            } catch (final IOException e) {
2347                throw new IllegalStateException("IOException while writing text body " +
2348                        "for message id " + Long.toString(messageId), e);
2349            }
2350        }
2351    }
2352
2353    /**
2354     * Writes a message body file to disk
2355     *
2356     * @param c Context for finding files dir
2357     * @param messageId id of message to write body for
2358     * @param ext "html" or "txt"
2359     * @param content Body content to write to file, or null/empty to delete file
2360     * @throws IOException
2361     */
2362    private static void writeBodyFile(final Context c, final long messageId, final String ext,
2363            final String content) throws IOException {
2364        final File textFile = getBodyFile(c, messageId, ext);
2365        if (TextUtils.isEmpty(content)) {
2366            if (!textFile.delete()) {
2367                LogUtils.v(LogUtils.TAG, "did not delete text body for %d", messageId);
2368            }
2369        } else {
2370            final FileWriter w = new FileWriter(textFile);
2371            try {
2372                w.write(content);
2373            } finally {
2374                w.close();
2375            }
2376        }
2377    }
2378
2379    /**
2380     * Returns a {@link java.io.File} object pointing to the body content file for the message
2381     *
2382     * @param c Context for finding files dir
2383     * @param messageId id of message to locate
2384     * @param ext "html" or "txt"
2385     * @return File ready for operating upon
2386     */
2387    protected static File getBodyFile(final Context c, final long messageId, final String ext)
2388            throws FileNotFoundException {
2389        if (!TextUtils.equals(ext, "html") && !TextUtils.equals(ext, "txt")) {
2390            throw new IllegalArgumentException("ext must be one of 'html' or 'txt'");
2391        }
2392        long l1 = messageId / 100 % 100;
2393        long l2 = messageId % 100;
2394        final File dir = new File(c.getFilesDir(),
2395                "body/" + Long.toString(l1) + "/" + Long.toString(l2) + "/");
2396        if (!dir.isDirectory() && !dir.mkdirs()) {
2397            throw new FileNotFoundException("Could not create directory for body file");
2398        }
2399        return new File(dir, Long.toString(messageId) + "." + ext);
2400    }
2401
2402    @Override
2403    public ParcelFileDescriptor openFile(final Uri uri, final String mode)
2404            throws FileNotFoundException {
2405        if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
2406            LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
2407        }
2408
2409        final int match = findMatch(uri, "openFile");
2410        switch (match) {
2411            case ATTACHMENTS_CACHED_FILE_ACCESS:
2412                // Parse the cache file path out from the uri
2413                final String cachedFilePath =
2414                        uri.getQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM);
2415
2416                if (cachedFilePath != null) {
2417                    // clearCallingIdentity means that the download manager will
2418                    // check our permissions rather than the permissions of whatever
2419                    // code is calling us.
2420                    long binderToken = Binder.clearCallingIdentity();
2421                    try {
2422                        LogUtils.d(TAG, "Opening attachment %s", cachedFilePath);
2423                        return ParcelFileDescriptor.open(
2424                                new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
2425                    } finally {
2426                        Binder.restoreCallingIdentity(binderToken);
2427                    }
2428                }
2429                break;
2430            case BODY_HTML: {
2431                final long messageKey = Long.valueOf(uri.getLastPathSegment());
2432                return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "html"),
2433                        Utilities.parseMode(mode));
2434            }
2435            case BODY_TEXT:{
2436                final long messageKey = Long.valueOf(uri.getLastPathSegment());
2437                return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "txt"),
2438                        Utilities.parseMode(mode));
2439            }
2440        }
2441
2442        throw new FileNotFoundException("unable to open file");
2443    }
2444
2445
2446    /**
2447     * Returns the base notification URI for the given content type.
2448     *
2449     * @param match The type of content that was modified.
2450     */
2451    private static Uri getBaseNotificationUri(int match) {
2452        Uri baseUri = null;
2453        switch (match) {
2454            case MESSAGE:
2455            case MESSAGE_ID:
2456            case SYNCED_MESSAGE_ID:
2457                baseUri = Message.NOTIFIER_URI;
2458                break;
2459            case ACCOUNT:
2460            case ACCOUNT_ID:
2461                baseUri = Account.NOTIFIER_URI;
2462                break;
2463        }
2464        return baseUri;
2465    }
2466
2467    /**
2468     * Sends a change notification to any cursors observers of the given base URI. The final
2469     * notification URI is dynamically built to contain the specified information. It will be
2470     * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending
2471     * upon the given values.
2472     * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked.
2473     * If this is necessary, it can be added. However, due to the implementation of
2474     * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications.
2475     *
2476     * @param baseUri The base URI to send notifications to. Must be able to take appended IDs.
2477     * @param op Optional operation to be appended to the URI.
2478     * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be
2479     *           appended to the base URI.
2480     */
2481    private void sendNotifierChange(Uri baseUri, String op, String id) {
2482        if (baseUri == null) return;
2483
2484        // Append the operation, if specified
2485        if (op != null) {
2486            baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
2487        }
2488
2489        long longId = 0L;
2490        try {
2491            longId = Long.valueOf(id);
2492        } catch (NumberFormatException ignore) {}
2493        if (longId > 0) {
2494            notifyUI(baseUri, id);
2495        } else {
2496            notifyUI(baseUri, null);
2497        }
2498
2499        // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI.
2500        if (baseUri.equals(Message.NOTIFIER_URI)) {
2501            sendMessageListDataChangedNotification();
2502        }
2503    }
2504
2505    private void sendMessageListDataChangedNotification() {
2506        final Context context = getContext();
2507        final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
2508        // Ideally this intent would contain information about which account changed, to limit the
2509        // updates to that particular account.  Unfortunately, that information is not available in
2510        // sendNotifierChange().
2511        context.sendBroadcast(intent);
2512    }
2513
2514    // We might have more than one thread trying to make its way through applyBatch() so the
2515    // notification coalescing needs to be thread-local to work correctly.
2516    private final ThreadLocal<Set<Uri>> mTLBatchNotifications =
2517            new ThreadLocal<Set<Uri>>();
2518
2519    private Set<Uri> getBatchNotificationsSet() {
2520        return mTLBatchNotifications.get();
2521    }
2522
2523    private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
2524        mTLBatchNotifications.set(batchNotifications);
2525    }
2526
2527    @Override
2528    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2529            throws OperationApplicationException {
2530        /**
2531         * Collect notification URIs to notify at the end of batch processing.
2532         * These are populated by calls to notifyUI() by way of update(), insert() and delete()
2533         * calls made in super.applyBatch()
2534         */
2535        setBatchNotificationsSet(Sets.<Uri>newHashSet());
2536        Context context = getContext();
2537        SQLiteDatabase db = getDatabase(context);
2538        db.beginTransaction();
2539        try {
2540            ContentProviderResult[] results = super.applyBatch(operations);
2541            db.setTransactionSuccessful();
2542            return results;
2543        } finally {
2544            db.endTransaction();
2545            final Set<Uri> notifications = getBatchNotificationsSet();
2546            setBatchNotificationsSet(null);
2547            for (final Uri uri : notifications) {
2548                context.getContentResolver().notifyChange(uri, null);
2549            }
2550        }
2551    }
2552
2553    public static interface EmailAttachmentService {
2554        /**
2555         * Notify the service that an attachment has changed.
2556         */
2557        void attachmentChanged(final Context context, final long id, final int flags);
2558    }
2559
2560    private final EmailAttachmentService DEFAULT_ATTACHMENT_SERVICE = new EmailAttachmentService() {
2561        @Override
2562        public void attachmentChanged(final Context context, final long id, final int flags) {
2563            // The default implementation delegates to the real service.
2564            AttachmentService.attachmentChanged(context, id, flags);
2565        }
2566    };
2567    private EmailAttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
2568
2569    // exposed for testing
2570    public void injectAttachmentService(final EmailAttachmentService attachmentService) {
2571        mAttachmentService =
2572            attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService;
2573    }
2574
2575    private Cursor notificationQuery(final Uri uri) {
2576        final SQLiteDatabase db = getDatabase(getContext());
2577        final String accountId = uri.getLastPathSegment();
2578
2579        final String sql = "SELECT " + MessageColumns.MAILBOX_KEY + ", " +
2580                "SUM(CASE " + MessageColumns.FLAG_READ + " WHEN 0 THEN 1 ELSE 0 END), " +
2581                "SUM(CASE " + MessageColumns.FLAG_SEEN + " WHEN 0 THEN 1 ELSE 0 END)\n" +
2582                "FROM " + Message.TABLE_NAME + "\n" +
2583                "WHERE " + MessageColumns.ACCOUNT_KEY + " = ?\n" +
2584                "GROUP BY " + MessageColumns.MAILBOX_KEY;
2585
2586        final String[] selectionArgs = {accountId};
2587
2588        return db.rawQuery(sql, selectionArgs);
2589    }
2590
2591    public Cursor mostRecentMessageQuery(Uri uri) {
2592        SQLiteDatabase db = getDatabase(getContext());
2593        String mailboxId = uri.getLastPathSegment();
2594        return db.rawQuery("select max(_id) from Message where mailboxKey=?",
2595                new String[] {mailboxId});
2596    }
2597
2598    private Cursor getMailboxMessageCount(Uri uri) {
2599        SQLiteDatabase db = getDatabase(getContext());
2600        String mailboxId = uri.getLastPathSegment();
2601        return db.rawQuery("select count(*) from Message where mailboxKey=?",
2602                new String[] {mailboxId});
2603    }
2604
2605    /**
2606     * Support for UnifiedEmail below
2607     */
2608
2609    private static final String NOT_A_DRAFT_STRING =
2610        Integer.toString(UIProvider.DraftType.NOT_A_DRAFT);
2611
2612    private static final String CONVERSATION_FLAGS =
2613            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
2614                ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE +
2615                " ELSE 0 END + " +
2616            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED +
2617                ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED +
2618                " ELSE 0 END + " +
2619             "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO +
2620                ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED +
2621                " ELSE 0 END";
2622
2623    /**
2624     * Array of pre-defined account colors (legacy colors from old email app)
2625     */
2626    private static final int[] ACCOUNT_COLORS = new int[] {
2627        0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79,
2628        0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4
2629    };
2630
2631    private static final String CONVERSATION_COLOR =
2632            "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length +
2633                    " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
2634                    " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
2635                    " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
2636                    " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
2637                    " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
2638                    " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
2639                    " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
2640                    " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
2641                    " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
2642            " END";
2643
2644    private static final String ACCOUNT_COLOR =
2645            "@CASE (" + AccountColumns._ID + " - 1) % " + ACCOUNT_COLORS.length +
2646                    " WHEN 0 THEN " + ACCOUNT_COLORS[0] +
2647                    " WHEN 1 THEN " + ACCOUNT_COLORS[1] +
2648                    " WHEN 2 THEN " + ACCOUNT_COLORS[2] +
2649                    " WHEN 3 THEN " + ACCOUNT_COLORS[3] +
2650                    " WHEN 4 THEN " + ACCOUNT_COLORS[4] +
2651                    " WHEN 5 THEN " + ACCOUNT_COLORS[5] +
2652                    " WHEN 6 THEN " + ACCOUNT_COLORS[6] +
2653                    " WHEN 7 THEN " + ACCOUNT_COLORS[7] +
2654                    " WHEN 8 THEN " + ACCOUNT_COLORS[8] +
2655            " END";
2656
2657    /**
2658     * Mapping of UIProvider columns to EmailProvider columns for the message list (called the
2659     * conversation list in UnifiedEmail)
2660     */
2661    private static ProjectionMap getMessageListMap() {
2662        if (sMessageListMap == null) {
2663            sMessageListMap = ProjectionMap.builder()
2664                .add(BaseColumns._ID, MessageColumns._ID)
2665                .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage"))
2666                .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage"))
2667                .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT)
2668                .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET)
2669                .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null)
2670                .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
2671                .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
2672                .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1")
2673                .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0")
2674                .add(UIProvider.ConversationColumns.SENDING_STATE,
2675                        Integer.toString(ConversationSendingState.OTHER))
2676                .add(UIProvider.ConversationColumns.PRIORITY,
2677                        Integer.toString(ConversationPriority.LOW))
2678                .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ)
2679                .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN)
2680                .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE)
2681                .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS)
2682                .add(UIProvider.ConversationColumns.ACCOUNT_URI,
2683                        uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
2684                .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST)
2685                .add(UIProvider.ConversationColumns.ORDER_KEY, MessageColumns.TIMESTAMP)
2686                .build();
2687        }
2688        return sMessageListMap;
2689    }
2690    private static ProjectionMap sMessageListMap;
2691
2692    /**
2693     * Generate UIProvider draft type; note the test for "reply all" must come before "reply"
2694     */
2695    private static final String MESSAGE_DRAFT_TYPE =
2696        "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL +
2697            ") !=0 THEN " + UIProvider.DraftType.COMPOSE +
2698        " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL +
2699            ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL +
2700        " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY +
2701            ") !=0 THEN " + UIProvider.DraftType.REPLY +
2702        " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD +
2703            ") !=0 THEN " + UIProvider.DraftType.FORWARD +
2704            " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END";
2705
2706    private static final String MESSAGE_FLAGS =
2707            "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE +
2708            ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE +
2709            " ELSE 0 END";
2710
2711    /**
2712     * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in
2713     * UnifiedEmail
2714     */
2715    private static ProjectionMap getMessageViewMap() {
2716        if (sMessageViewMap == null) {
2717            sMessageViewMap = ProjectionMap.builder()
2718                .add(BaseColumns._ID, Message.TABLE_NAME + "." + MessageColumns._ID)
2719                .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID)
2720                .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME))
2721                .add(UIProvider.MessageColumns.CONVERSATION_ID,
2722                        uriWithFQId("uimessage", Message.TABLE_NAME))
2723                .add(UIProvider.MessageColumns.SUBJECT, MessageColumns.SUBJECT)
2724                .add(UIProvider.MessageColumns.SNIPPET, MessageColumns.SNIPPET)
2725                .add(UIProvider.MessageColumns.FROM, MessageColumns.FROM_LIST)
2726                .add(UIProvider.MessageColumns.TO, MessageColumns.TO_LIST)
2727                .add(UIProvider.MessageColumns.CC, MessageColumns.CC_LIST)
2728                .add(UIProvider.MessageColumns.BCC, MessageColumns.BCC_LIST)
2729                .add(UIProvider.MessageColumns.REPLY_TO, MessageColumns.REPLY_TO_LIST)
2730                .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP)
2731                .add(UIProvider.MessageColumns.BODY_HTML, null) // Loaded in EmailMessageCursor
2732                .add(UIProvider.MessageColumns.BODY_TEXT, null) // Loaded in EmailMessageCursor
2733                .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0")
2734                .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING)
2735                .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0")
2736                .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT)
2737                .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI,
2738                        uriWithFQId("uiattachments", Message.TABLE_NAME))
2739                .add(UIProvider.MessageColumns.ATTACHMENT_BY_CID_URI,
2740                        uriWithFQId("uiattachmentbycid", Message.TABLE_NAME))
2741                .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS)
2742                .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE)
2743                .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI,
2744                        uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY))
2745                .add(UIProvider.MessageColumns.STARRED, MessageColumns.FLAG_FAVORITE)
2746                .add(UIProvider.MessageColumns.READ, MessageColumns.FLAG_READ)
2747                .add(UIProvider.MessageColumns.SEEN, MessageColumns.FLAG_SEEN)
2748                .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null)
2749                .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL,
2750                        Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING))
2751                .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE,
2752                        Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK))
2753                .add(UIProvider.MessageColumns.VIA_DOMAIN, null)
2754                .add(UIProvider.MessageColumns.CLIPPED, "0")
2755                .add(UIProvider.MessageColumns.PERMALINK, null)
2756                .build();
2757        }
2758        return sMessageViewMap;
2759    }
2760    private static ProjectionMap sMessageViewMap;
2761
2762    /**
2763     * Generate UIProvider folder capabilities from mailbox flags
2764     */
2765    private static final String FOLDER_CAPABILITIES =
2766        "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL +
2767            ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES +
2768            " ELSE 0 END";
2769
2770    /**
2771     * Convert EmailProvider type to UIProvider type
2772     */
2773    private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE
2774            + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + UIProvider.FolderType.INBOX
2775            + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + UIProvider.FolderType.DRAFT
2776            + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + UIProvider.FolderType.OUTBOX
2777            + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + UIProvider.FolderType.SENT
2778            + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + UIProvider.FolderType.TRASH
2779            + " WHEN " + Mailbox.TYPE_JUNK    + " THEN " + UIProvider.FolderType.SPAM
2780            + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED
2781            + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD
2782            + " WHEN " + Mailbox.TYPE_SEARCH + " THEN "
2783                    + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH)
2784            + " ELSE " + UIProvider.FolderType.DEFAULT + " END";
2785
2786    private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE
2787            + " WHEN " + Mailbox.TYPE_INBOX   + " THEN " + R.drawable.ic_drawer_inbox_24dp
2788            + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN " + R.drawable.ic_drawer_drafts_24dp
2789            + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN " + R.drawable.ic_drawer_outbox_24dp
2790            + " WHEN " + Mailbox.TYPE_SENT    + " THEN " + R.drawable.ic_drawer_sent_24dp
2791            + " WHEN " + Mailbox.TYPE_TRASH   + " THEN " + R.drawable.ic_drawer_trash_24dp
2792            + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_drawer_starred_24dp
2793            + " ELSE " + R.drawable.ic_drawer_folder_24dp + " END";
2794
2795    /**
2796     * Local-only folders set totalCount < 0; such folders should substitute message count for
2797     * total count.
2798     * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types.
2799     */
2800    private static final String TOTAL_COUNT = "CASE WHEN "
2801            + MailboxColumns.TOTAL_COUNT + "<0 OR "
2802            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR "
2803            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR "
2804            + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH
2805            + " THEN " + MailboxColumns.MESSAGE_COUNT
2806            + " ELSE " + MailboxColumns.TOTAL_COUNT + " END";
2807
2808    private static ProjectionMap getFolderListMap() {
2809        if (sFolderListMap == null) {
2810            sFolderListMap = ProjectionMap.builder()
2811                .add(BaseColumns._ID, MailboxColumns._ID)
2812                .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID)
2813                .add(UIProvider.FolderColumns.URI, uriWithId("uifolder"))
2814                .add(UIProvider.FolderColumns.NAME, "displayName")
2815                .add(UIProvider.FolderColumns.HAS_CHILDREN,
2816                        MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN)
2817                .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES)
2818                .add(UIProvider.FolderColumns.SYNC_WINDOW, "3")
2819                .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages"))
2820                .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders"))
2821                .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT)
2822                .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT)
2823                .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH))
2824                .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS)
2825                .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT)
2826                .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE)
2827                .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON)
2828                .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore"))
2829                .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME)
2830                .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY
2831                        + "=" + Mailbox.NO_MAILBOX + " then NULL else " +
2832                        uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end")
2833                /**
2834                 * SELECT group_concat(fromList) FROM
2835                 * (SELECT fromList FROM message WHERE mailboxKey=? AND flagRead=0
2836                 *  GROUP BY fromList ORDER BY timestamp DESC)
2837                 */
2838                .add(UIProvider.FolderColumns.UNREAD_SENDERS,
2839                        "(SELECT group_concat(" + MessageColumns.FROM_LIST + ") FROM " +
2840                        "(SELECT " + MessageColumns.FROM_LIST + " FROM " + Message.TABLE_NAME +
2841                        " WHERE " + MessageColumns.MAILBOX_KEY + "=" + Mailbox.TABLE_NAME + "." +
2842                        MailboxColumns._ID + " AND " + MessageColumns.FLAG_READ + "=0" +
2843                        " GROUP BY " + MessageColumns.FROM_LIST + " ORDER BY " +
2844                        MessageColumns.TIMESTAMP + " DESC))")
2845                .build();
2846        }
2847        return sFolderListMap;
2848    }
2849    private static ProjectionMap sFolderListMap;
2850
2851    /**
2852     * Constructs the map of default entries for accounts. These values can be overridden in
2853     * {@link #genQueryAccount(String[], String)}.
2854     */
2855    private static ProjectionMap getAccountListMap(Context context) {
2856        if (sAccountListMap == null) {
2857            final ProjectionMap.Builder builder = ProjectionMap.builder()
2858                    .add(BaseColumns._ID, AccountColumns._ID)
2859                    .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders"))
2860                    .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders"))
2861                    .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders"))
2862                    .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME)
2863                    .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME,
2864                            AccountColumns.EMAIL_ADDRESS)
2865                    .add(UIProvider.AccountColumns.ACCOUNT_ID,
2866                            AccountColumns.EMAIL_ADDRESS)
2867                    .add(UIProvider.AccountColumns.SENDER_NAME,
2868                            AccountColumns.SENDER_NAME)
2869                    .add(UIProvider.AccountColumns.UNDO_URI,
2870                            ("'content://" + EmailContent.AUTHORITY + "/uiundo'"))
2871                    .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount"))
2872                    .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch"))
2873                            // TODO: Is provider version used?
2874                    .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1")
2875                    .add(UIProvider.AccountColumns.SYNC_STATUS, "0")
2876                    .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI,
2877                            uriWithId("uirecentfolders"))
2878                    .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI,
2879                            uriWithId("uidefaultrecentfolders"))
2880                    .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE,
2881                            AccountColumns.SIGNATURE)
2882                    .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS,
2883                            Integer.toString(UIProvider.SnapHeaderValue.ALWAYS))
2884                    .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0")
2885                    .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE,
2886                            Integer.toString(UIProvider.ConversationViewMode.UNDEFINED))
2887                    .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null);
2888
2889            final String feedbackUri = context.getString(R.string.email_feedback_uri);
2890            if (!TextUtils.isEmpty(feedbackUri)) {
2891                // This string needs to be in single quotes, as it will be used as a constant
2892                // in a sql expression
2893                builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI,
2894                        "'" + feedbackUri + "'");
2895            }
2896
2897            final String helpUri = context.getString(R.string.help_uri);
2898            if (!TextUtils.isEmpty(helpUri)) {
2899                // This string needs to be in single quotes, as it will be used as a constant
2900                // in a sql expression
2901                builder.add(UIProvider.AccountColumns.HELP_INTENT_URI,
2902                        "'" + helpUri + "'");
2903            }
2904
2905            sAccountListMap = builder.build();
2906        }
2907        return sAccountListMap;
2908    }
2909    private static ProjectionMap sAccountListMap;
2910
2911    private static ProjectionMap getQuickResponseMap() {
2912        if (sQuickResponseMap == null) {
2913            sQuickResponseMap = ProjectionMap.builder()
2914                    .add(UIProvider.QuickResponseColumns.TEXT, QuickResponseColumns.TEXT)
2915                    .add(UIProvider.QuickResponseColumns.URI,
2916                            "'" + combinedUriString("quickresponse", "") + "'||"
2917                                    + QuickResponseColumns._ID)
2918                    .build();
2919        }
2920        return sQuickResponseMap;
2921    }
2922    private static ProjectionMap sQuickResponseMap;
2923
2924    /**
2925     * The "ORDER BY" clause for top level folders
2926     */
2927    private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE
2928        + " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0"
2929        + " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1"
2930        + " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2"
2931        + " WHEN " + Mailbox.TYPE_SENT    + " THEN 3"
2932        + " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4"
2933        + " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5"
2934        // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
2935        + " ELSE 10 END"
2936        + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
2937
2938    /**
2939     * Mapping of UIProvider columns to EmailProvider columns for a message's attachments
2940     */
2941    private static ProjectionMap getAttachmentMap() {
2942        if (sAttachmentMap == null) {
2943            sAttachmentMap = ProjectionMap.builder()
2944                .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME)
2945                .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE)
2946                .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment"))
2947                .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE)
2948                .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE)
2949                .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION)
2950                .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE,
2951                        AttachmentColumns.UI_DOWNLOADED_SIZE)
2952                .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI)
2953                .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS)
2954                .build();
2955        }
2956        return sAttachmentMap;
2957    }
2958    private static ProjectionMap sAttachmentMap;
2959
2960    /**
2961     * Generate the SELECT clause using a specified mapping and the original UI projection
2962     * @param map the ProjectionMap to use for this projection
2963     * @param projection the projection as sent by UnifiedEmail
2964     * @return a StringBuilder containing the SELECT expression for a SQLite query
2965     */
2966    private static StringBuilder genSelect(ProjectionMap map, String[] projection) {
2967        return genSelect(map, projection, EMPTY_CONTENT_VALUES);
2968    }
2969
2970    private static StringBuilder genSelect(ProjectionMap map, String[] projection,
2971            ContentValues values) {
2972        final StringBuilder sb = new StringBuilder("SELECT ");
2973        boolean first = true;
2974        for (final String column: projection) {
2975            if (first) {
2976                first = false;
2977            } else {
2978                sb.append(',');
2979            }
2980            final String val;
2981            // First look at values; this is an override of default behavior
2982            if (values.containsKey(column)) {
2983                final String value = values.getAsString(column);
2984                if (value == null) {
2985                    val = "NULL AS " + column;
2986                } else if (value.startsWith("@")) {
2987                    val = value.substring(1) + " AS " + column;
2988                } else {
2989                    val = DatabaseUtils.sqlEscapeString(value) + " AS " + column;
2990                }
2991            } else {
2992                // Now, get the standard value for the column from our projection map
2993                final String mapVal = map.get(column);
2994                // If we don't have the column, return "NULL AS <column>", and warn
2995                if (mapVal == null) {
2996                    val = "NULL AS " + column;
2997                    // Apparently there's a lot of these, so don't spam the log with warnings
2998                    // LogUtils.w(TAG, "column " + column + " missing from projection map");
2999                } else {
3000                    val = mapVal;
3001                }
3002            }
3003            sb.append(val);
3004        }
3005        return sb;
3006    }
3007
3008    /**
3009     * Convenience method to create a Uri string given the "type" of query; we append the type
3010     * of the query and the id column name (_id)
3011     *
3012     * @param type the "type" of the query, as defined by our UriMatcher definitions
3013     * @return a Uri string
3014     */
3015    private static String uriWithId(String type) {
3016        return uriWithColumn(type, BaseColumns._ID);
3017    }
3018
3019    /**
3020     * Convenience method to create a Uri string given the "type" of query; we append the type
3021     * of the query and the passed in column name
3022     *
3023     * @param type the "type" of the query, as defined by our UriMatcher definitions
3024     * @param columnName the column in the table being queried
3025     * @return a Uri string
3026     */
3027    private static String uriWithColumn(String type, String columnName) {
3028        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName;
3029    }
3030
3031    /**
3032     * Convenience method to create a Uri string given the "type" of query and the table name to
3033     * which it applies; we append the type of the query and the fully qualified (FQ) id column
3034     * (i.e. including the table name); we need this for join queries where _id would otherwise
3035     * be ambiguous
3036     *
3037     * @param type the "type" of the query, as defined by our UriMatcher definitions
3038     * @param tableName the name of the table whose _id is referred to
3039     * @return a Uri string
3040     */
3041    private static String uriWithFQId(String type, String tableName) {
3042        return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id";
3043    }
3044
3045    // Regex that matches start of img tag. '<(?i)img\s+'.
3046    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
3047
3048    /**
3049     * Class that holds the sqlite query and the attachment (JSON) value (which might be null)
3050     */
3051    private static class MessageQuery {
3052        final String query;
3053        final String attachmentJson;
3054
3055        MessageQuery(String _query, String _attachmentJson) {
3056            query = _query;
3057            attachmentJson = _attachmentJson;
3058        }
3059    }
3060
3061    /**
3062     * Generate the "view message" SQLite query, given a projection from UnifiedEmail
3063     *
3064     * @param uiProjection as passed from UnifiedEmail
3065     * @return the SQLite query to be executed on the EmailProvider database
3066     */
3067    private MessageQuery genQueryViewMessage(String[] uiProjection, String id) {
3068        Context context = getContext();
3069        long messageId = Long.parseLong(id);
3070        Message msg = Message.restoreMessageWithId(context, messageId);
3071        ContentValues values = new ContentValues();
3072        String attachmentJson = null;
3073        if (msg != null) {
3074            Body body = Body.restoreBodyWithMessageId(context, messageId);
3075            if (body != null) {
3076                if (body.mHtmlContent != null) {
3077                    if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) {
3078                        values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1);
3079                    }
3080                }
3081            }
3082            Address[] fromList = Address.fromHeader(msg.mFrom);
3083            int autoShowImages = 0;
3084            final MailPrefs mailPrefs = MailPrefs.get(context);
3085            for (Address sender : fromList) {
3086                final String email = sender.getAddress();
3087                if (mailPrefs.getDisplayImagesFromSender(email)) {
3088                    autoShowImages = 1;
3089                    break;
3090                }
3091            }
3092            values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages);
3093            // Add attachments...
3094            Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId);
3095            if (atts.length > 0) {
3096                ArrayList<com.android.mail.providers.Attachment> uiAtts =
3097                        new ArrayList<com.android.mail.providers.Attachment>();
3098                for (Attachment att : atts) {
3099                    // TODO: This code is intended to strip out any inlined attachments (which
3100                    // would have a non-null contentId) so that they will not display at the bottom
3101                    // along with the non-inlined attachments.
3102                    // The problem is that the UI_ATTACHMENTS query does not behave the same way,
3103                    // which causes crazy formatting.
3104                    // There is an open question here, should attachments that are inlined
3105                    // ALSO appear in the list of attachments at the bottom with the non-inlined
3106                    // attachments?
3107                    // Either way, the two queries need to behave the same way.
3108                    // As of now, they will. If we decide to stop this, then we need to enable
3109                    // the code below, and then also make the UI_ATTACHMENTS query behave
3110                    // the same way.
3111//
3112//                    if (att.mContentId != null && att.getContentUri() != null) {
3113//                        continue;
3114//                    }
3115                    com.android.mail.providers.Attachment uiAtt =
3116                            new com.android.mail.providers.Attachment();
3117                    uiAtt.setName(att.mFileName);
3118                    uiAtt.setContentType(att.mMimeType);
3119                    uiAtt.size = (int) att.mSize;
3120                    uiAtt.uri = uiUri("uiattachment", att.mId);
3121                    uiAtt.flags = att.mFlags;
3122                    uiAtts.add(uiAtt);
3123                }
3124                values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal
3125                attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts);
3126            }
3127            if (msg.mDraftInfo != 0) {
3128                values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT,
3129                        (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0);
3130                values.put(UIProvider.MessageColumns.QUOTE_START_POS,
3131                        msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK);
3132            }
3133            if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
3134                values.put(UIProvider.MessageColumns.EVENT_INTENT_URI,
3135                        "content://ui.email2.android.com/event/" + msg.mId);
3136            }
3137            /**
3138             * HACK: override the attachment uri to contain a query parameter
3139             * This forces the message footer to reload the attachment display when the message is
3140             * fully loaded.
3141             */
3142            final Uri attachmentListUri = uiUri("uiattachments", messageId).buildUpon()
3143                    .appendQueryParameter("MessageLoaded",
3144                            msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE ? "true" : "false")
3145                    .build();
3146            values.put(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, attachmentListUri.toString());
3147        }
3148        StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values);
3149        sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME +
3150                " ON " + BodyColumns.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." +
3151                        MessageColumns._ID +
3152                " WHERE " + Message.TABLE_NAME + "." + MessageColumns._ID + "=?");
3153        String sql = sb.toString();
3154        return new MessageQuery(sql, attachmentJson);
3155    }
3156
3157    private static void appendConversationInfoColumns(final StringBuilder stringBuilder) {
3158        // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :(
3159        // There may be a better way to do this, but since the projection is specified by the
3160        // unified UI code, it can't ask for these columns.
3161        stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME)
3162                .append(',').append(MessageColumns.FROM_LIST)
3163                .append(',').append(MessageColumns.TO_LIST);
3164    }
3165
3166    /**
3167     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
3168     *
3169     * @param uiProjection as passed from UnifiedEmail
3170     * @param unseenOnly <code>true</code> to only return unseen messages
3171     * @return the SQLite query to be executed on the EmailProvider database
3172     */
3173    private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) {
3174        StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
3175        appendConversationInfoColumns(sb);
3176        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
3177                Message.FLAG_LOADED_SELECTION + " AND " +
3178                MessageColumns.MAILBOX_KEY + "=? ");
3179        if (unseenOnly) {
3180            sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 ");
3181            sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 ");
3182        }
3183        sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC ");
3184        sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMIT);
3185        return sb.toString();
3186    }
3187
3188    /**
3189     * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail
3190     *
3191     * @param uiProjection as passed from UnifiedEmail
3192     * @param mailboxId the id of the virtual mailbox
3193     * @param unseenOnly <code>true</code> to only return unseen messages
3194     * @return the SQLite query to be executed on the EmailProvider database
3195     */
3196    private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection,
3197            long mailboxId, final boolean unseenOnly) {
3198        ContentValues values = new ContentValues();
3199        values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR);
3200        final int virtualMailboxId = getVirtualMailboxType(mailboxId);
3201        final String[] selectionArgs;
3202        StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values);
3203        appendConversationInfoColumns(sb);
3204        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " +
3205                Message.FLAG_LOADED_SELECTION + " AND ");
3206        if (isCombinedMailbox(mailboxId)) {
3207            if (unseenOnly) {
3208                sb.append(MessageColumns.FLAG_SEEN).append("=0 AND ");
3209                sb.append(MessageColumns.FLAG_READ).append("=0 AND ");
3210            }
3211            selectionArgs = null;
3212        } else {
3213            if (virtualMailboxId == Mailbox.TYPE_INBOX) {
3214                throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
3215            }
3216            sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND ");
3217            selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)};
3218        }
3219        switch (getVirtualMailboxType(mailboxId)) {
3220            case Mailbox.TYPE_INBOX:
3221                sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID +
3222                        " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE +
3223                        "=" + Mailbox.TYPE_INBOX + ")");
3224                break;
3225            case Mailbox.TYPE_STARRED:
3226                sb.append(MessageColumns.FLAG_FAVORITE + "=1");
3227                break;
3228            case Mailbox.TYPE_UNREAD:
3229                sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY +
3230                        " NOT IN (SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME +
3231                        " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")");
3232                break;
3233            default:
3234                throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId);
3235        }
3236        sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC");
3237        return db.rawQuery(sb.toString(), selectionArgs);
3238    }
3239
3240    /**
3241     * Generate the "message list" SQLite query, given a projection from UnifiedEmail
3242     *
3243     * @param uiProjection as passed from UnifiedEmail
3244     * @return the SQLite query to be executed on the EmailProvider database
3245     */
3246    private static String genQueryConversation(String[] uiProjection) {
3247        StringBuilder sb = genSelect(getMessageListMap(), uiProjection);
3248        sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + MessageColumns._ID + "=?");
3249        return sb.toString();
3250    }
3251
3252    /**
3253     * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail
3254     *
3255     * @param uiProjection as passed from UnifiedEmail
3256     * @return the SQLite query to be executed on the EmailProvider database
3257     */
3258    private static String genQueryAccountMailboxes(String[] uiProjection) {
3259        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
3260        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
3261                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
3262                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
3263                " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY ");
3264        sb.append(MAILBOX_ORDER_BY);
3265        return sb.toString();
3266    }
3267
3268    /**
3269     * Generate the "all folders" SQLite query, given a projection from UnifiedEmail.  The list is
3270     * sorted by the name as it appears in a hierarchical listing
3271     *
3272     * @param uiProjection as passed from UnifiedEmail
3273     * @return the SQLite query to be executed on the EmailProvider database
3274     */
3275    private static String genQueryAccountAllMailboxes(String[] uiProjection) {
3276        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
3277        // Use a derived column to choose either hierarchicalName or displayName
3278        sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " +
3279                MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME +
3280                " end as h_name");
3281        // Order by the derived column
3282        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
3283                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
3284                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
3285                " ORDER BY h_name");
3286        return sb.toString();
3287    }
3288
3289    /**
3290     * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail
3291     *
3292     * @param uiProjection as passed from UnifiedEmail
3293     * @return the SQLite query to be executed on the EmailProvider database
3294     */
3295    private static String genQueryRecentMailboxes(String[] uiProjection) {
3296        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
3297        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY +
3298                "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL +
3299                " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH +
3300                " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " +
3301                MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " +
3302                MailboxColumns.LAST_TOUCHED_TIME + " DESC");
3303        return sb.toString();
3304    }
3305
3306    private int getFolderCapabilities(EmailServiceInfo info, int mailboxType, long mailboxId) {
3307        // Special case for Search folders: only permit delete, do not try to give any other caps.
3308        if (mailboxType == Mailbox.TYPE_SEARCH) {
3309            return UIProvider.FolderCapabilities.DELETE;
3310        }
3311
3312        // All folders support delete, except drafts.
3313        int caps = 0;
3314        if (mailboxType != Mailbox.TYPE_DRAFTS) {
3315            caps = UIProvider.FolderCapabilities.DELETE;
3316        }
3317        if (info != null && info.offerLookback) {
3318            // Protocols supporting lookback support settings
3319            caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS;
3320        }
3321
3322        if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
3323                mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
3324            // If the mailbox can accept moved mail, report that as well
3325            caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES;
3326            caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION;
3327        }
3328
3329        // For trash, we don't allow undo
3330        if (mailboxType == Mailbox.TYPE_TRASH) {
3331            caps =  UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES |
3332                    UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION |
3333                    UIProvider.FolderCapabilities.DELETE |
3334                    UIProvider.FolderCapabilities.DELETE_ACTION_FINAL;
3335        }
3336        if (isVirtualMailbox(mailboxId)) {
3337            caps |= UIProvider.FolderCapabilities.IS_VIRTUAL;
3338        }
3339
3340        // If we don't know the protocol or the protocol doesn't support it, don't allow moving
3341        // messages
3342        if (info == null || !info.offerMoveTo) {
3343            caps &= ~UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES &
3344                    ~UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION &
3345                    ~UIProvider.FolderCapabilities.ALLOWS_MOVE_TO_INBOX;
3346        }
3347
3348        // If the mailbox stores outgoing mail, show recipients instead of senders
3349        // (however the Drafts folder shows neither senders nor recipients... just the word "Draft")
3350        if (mailboxType == Mailbox.TYPE_OUTBOX || mailboxType == Mailbox.TYPE_SENT) {
3351            caps |= UIProvider.FolderCapabilities.SHOW_RECIPIENTS;
3352        }
3353
3354        return caps;
3355    }
3356
3357    /**
3358     * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail
3359     *
3360     * @param uiProjection as passed from UnifiedEmail
3361     * @return the SQLite query to be executed on the EmailProvider database
3362     */
3363    private String genQueryMailbox(String[] uiProjection, String id) {
3364        long mailboxId = Long.parseLong(id);
3365        ContentValues values = new ContentValues(3);
3366        if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) {
3367            // "load more" is valid for search results
3368            values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
3369                    uiUriString("uiloadmore", mailboxId));
3370            values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE);
3371        } else {
3372            Context context = getContext();
3373            Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
3374            // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot)
3375            if (mailbox != null) {
3376                String protocol = Account.getProtocol(context, mailbox.mAccountKey);
3377                EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
3378                // All folders support delete
3379                if (info != null && info.offerLoadMore) {
3380                    // "load more" is valid for protocols not supporting "lookback"
3381                    values.put(UIProvider.FolderColumns.LOAD_MORE_URI,
3382                            uiUriString("uiloadmore", mailboxId));
3383                }
3384                values.put(UIProvider.FolderColumns.CAPABILITIES,
3385                        getFolderCapabilities(info, mailbox.mType, mailboxId));
3386                // The persistent id is used to form a filename, so we must ensure that it doesn't
3387                // include illegal characters (such as '/'). Only perform the encoding if this
3388                // query wants the persistent id.
3389                boolean shouldEncodePersistentId = false;
3390                if (uiProjection == null) {
3391                    shouldEncodePersistentId = true;
3392                } else {
3393                    for (final String column : uiProjection) {
3394                        if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) {
3395                            shouldEncodePersistentId = true;
3396                            break;
3397                        }
3398                    }
3399                }
3400                if (shouldEncodePersistentId) {
3401                    values.put(UIProvider.FolderColumns.PERSISTENT_ID,
3402                            Base64.encodeToString(mailbox.mServerId.getBytes(),
3403                                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
3404                }
3405             }
3406        }
3407        StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values);
3408        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns._ID + "=?");
3409        return sb.toString();
3410    }
3411
3412    public static final String LEGACY_AUTHORITY = "ui.email.android.com";
3413    private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY);
3414
3415    private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com");
3416
3417    private static String getExternalUriString(String segment, String account) {
3418        return BASE_EXTERNAL_URI.buildUpon().appendPath(segment)
3419                .appendQueryParameter("account", account).build().toString();
3420    }
3421
3422    private static String getExternalUriStringEmail2(String segment, String account) {
3423        return BASE_EXTERAL_URI2.buildUpon().appendPath(segment)
3424                .appendQueryParameter("account", account).build().toString();
3425    }
3426
3427    private static String getBits(int bitField) {
3428        StringBuilder sb = new StringBuilder(" ");
3429        for (int i = 0; i < 32; i++, bitField >>= 1) {
3430            if ((bitField & 1) != 0) {
3431                sb.append(i)
3432                        .append(" ");
3433            }
3434        }
3435        return sb.toString();
3436    }
3437
3438    private static int getCapabilities(Context context, final Account account) {
3439        if (account == null) {
3440            return 0;
3441        }
3442        // Account capabilities are based on protocol -- different protocols (and, for EAS,
3443        // different protocol versions) support different feature sets.
3444        final String protocol = account.getProtocol(context);
3445        int capabilities;
3446        if (TextUtils.equals(context.getString(R.string.protocol_imap), protocol) ||
3447                TextUtils.equals(context.getString(R.string.protocol_legacy_imap), protocol)) {
3448            capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
3449                    AccountCapabilities.SERVER_SEARCH |
3450                    AccountCapabilities.FOLDER_SERVER_SEARCH |
3451                    AccountCapabilities.UNDO |
3452                    AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
3453        } else if (TextUtils.equals(context.getString(R.string.protocol_pop3), protocol)) {
3454            capabilities = AccountCapabilities.UNDO |
3455                    AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
3456        } else if (TextUtils.equals(context.getString(R.string.protocol_eas), protocol)) {
3457            final String easVersion = account.mProtocolVersion;
3458            double easVersionDouble = 2.5D;
3459            if (easVersion != null) {
3460                try {
3461                    easVersionDouble = Double.parseDouble(easVersion);
3462                } catch (final NumberFormatException e) {
3463                    // Use the default (lowest) set of capabilities.
3464                }
3465            }
3466            if (easVersionDouble >= 12.0D) {
3467                capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
3468                        AccountCapabilities.SERVER_SEARCH |
3469                        AccountCapabilities.FOLDER_SERVER_SEARCH |
3470                        AccountCapabilities.SMART_REPLY |
3471                        AccountCapabilities.UNDO |
3472                        AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
3473            } else {
3474                capabilities = AccountCapabilities.SYNCABLE_FOLDERS |
3475                        AccountCapabilities.SMART_REPLY |
3476                        AccountCapabilities.UNDO |
3477                        AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
3478            }
3479        } else {
3480            LogUtils.w(TAG, "Unknown protocol for account %d", account.getId());
3481            return 0;
3482        }
3483        LogUtils.d(TAG, "getCapabilities() for %d (protocol %s): 0x%x %s", account.getId(), protocol,
3484                capabilities, getBits(capabilities));
3485
3486        // If the configuration states that feedback is supported, add that capability
3487        final Resources res = context.getResources();
3488        if (res.getBoolean(R.bool.feedback_supported)) {
3489            capabilities |= AccountCapabilities.SEND_FEEDBACK;
3490        }
3491
3492        // If we can find a help URL then add the Help capability
3493        if (!TextUtils.isEmpty(context.getResources().getString(R.string.help_uri))) {
3494            capabilities |= AccountCapabilities.HELP_CONTENT;
3495        }
3496
3497        capabilities |= AccountCapabilities.EMPTY_TRASH;
3498
3499        // TODO: Should this be stored per-account, or some other mechanism?
3500        capabilities |= AccountCapabilities.NESTED_FOLDERS;
3501
3502        // the client is permitted to sanitize HTML emails for all Email accounts
3503        capabilities |= AccountCapabilities.CLIENT_SANITIZED_HTML;
3504
3505        return capabilities;
3506    }
3507
3508    /**
3509     * Generate a "single account" SQLite query, given a projection from UnifiedEmail
3510     *
3511     * @param uiProjection as passed from UnifiedEmail
3512     * @param id account row ID
3513     * @return the SQLite query to be executed on the EmailProvider database
3514     */
3515    private String genQueryAccount(String[] uiProjection, String id) {
3516        final ContentValues values = new ContentValues();
3517        final long accountId = Long.parseLong(id);
3518        final Context context = getContext();
3519
3520        EmailServiceInfo info = null;
3521
3522        // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null.
3523        final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection);
3524
3525        final Account account = Account.restoreAccountWithId(context, accountId);
3526        if (account == null) {
3527            LogUtils.d(TAG, "Account %d not found during genQueryAccount", accountId);
3528        }
3529        if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) {
3530            // Get account capabilities from the service
3531            values.put(UIProvider.AccountColumns.CAPABILITIES,
3532                    (account == null ? 0 : getCapabilities(context, account)));
3533        }
3534        if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
3535            values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI,
3536                    getExternalUriString("settings", id));
3537        }
3538        if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) {
3539            values.put(UIProvider.AccountColumns.COMPOSE_URI,
3540                    getExternalUriStringEmail2("compose", id));
3541        }
3542        if (projectionColumns.contains(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI)) {
3543            values.put(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI,
3544                    getIncomingSettingsUri(accountId).toString());
3545        }
3546        if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) {
3547            values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE);
3548        }
3549        if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) {
3550            values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR);
3551        }
3552
3553        // TODO: if we're getting the values out of MailPrefs then we don't need to be passing the
3554        // values this way
3555        final MailPrefs mailPrefs = MailPrefs.get(getContext());
3556        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
3557            values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE,
3558                    mailPrefs.getConfirmDelete() ? "1" : "0");
3559        }
3560        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
3561            values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND,
3562                    mailPrefs.getConfirmSend() ? "1" : "0");
3563        }
3564        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) {
3565            values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE,
3566                    mailPrefs.getConversationListSwipeActionInteger(false));
3567        }
3568        if (projectionColumns.contains(
3569                UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
3570            values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON,
3571                    getConversationListIcon(mailPrefs));
3572        }
3573        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
3574            values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE,
3575                    Integer.toString(mailPrefs.getAutoAdvanceMode()));
3576        }
3577        // Set default inbox, if we've got an inbox; otherwise, say initial sync needed
3578        final long inboxMailboxId =
3579                Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
3580        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) &&
3581                inboxMailboxId != Mailbox.NO_MAILBOX) {
3582            values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
3583                    uiUriString("uifolder", inboxMailboxId));
3584        } else {
3585            values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX,
3586                    uiUriString("uiinbox", accountId));
3587        }
3588        if (projectionColumns.contains(
3589                UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) &&
3590                inboxMailboxId != Mailbox.NO_MAILBOX) {
3591            values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME,
3592                    Mailbox.getDisplayName(context, inboxMailboxId));
3593        }
3594        if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) {
3595            if (inboxMailboxId != Mailbox.NO_MAILBOX) {
3596                values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
3597            } else {
3598                values.put(UIProvider.AccountColumns.SYNC_STATUS,
3599                        UIProvider.SyncStatus.INITIAL_SYNC_NEEDED);
3600            }
3601        }
3602        if (projectionColumns.contains(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) {
3603            values.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI,
3604                    uiUriString("uiacctsettings", -1));
3605        }
3606        if (projectionColumns.contains(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS)) {
3607            // Email is now sanitized, which grants the ability to inject beautifying javascript.
3608            values.put(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS, 1);
3609        }
3610        if (projectionColumns.contains(UIProvider.AccountColumns.SECURITY_HOLD)) {
3611            final int hold = ((account != null &&
3612                    ((account.getFlags() & Account.FLAGS_SECURITY_HOLD) == 0)) ? 0 : 1);
3613            values.put(UIProvider.AccountColumns.SECURITY_HOLD, hold);
3614        }
3615        if (projectionColumns.contains(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) {
3616            values.put(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI,
3617                    (account == null ? "" : AccountSecurity.getUpdateSecurityUri(
3618                            account.getId(), true).toString()));
3619        }
3620        if (projectionColumns.contains(
3621                UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED)) {
3622            // Email doesn't support priority inbox, so always state importance markers disabled.
3623            values.put(UIProvider.AccountColumns.SettingsColumns.IMPORTANCE_MARKERS_ENABLED, "0");
3624        }
3625        if (projectionColumns.contains(
3626                UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED)) {
3627            // Email doesn't support priority inbox, so always state show chevrons disabled.
3628            values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_CHEVRONS_ENABLED, "0");
3629        }
3630        if (projectionColumns.contains(
3631                UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) {
3632            // Set the setup intent if needed
3633            // TODO We should clarify/document the trash/setup relationship
3634            long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH);
3635            if (trashId == Mailbox.NO_MAILBOX) {
3636                info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
3637                if (info != null && info.requiresSetup) {
3638                    values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI,
3639                            getExternalUriString("setup", id));
3640                }
3641            }
3642        }
3643        if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) {
3644            final String type;
3645            if (info == null) {
3646                info = EmailServiceUtils.getServiceInfoForAccount(context, accountId);
3647            }
3648            if (info != null) {
3649                type = info.accountType;
3650            } else {
3651                type = "unknown";
3652            }
3653
3654            values.put(UIProvider.AccountColumns.TYPE, type);
3655        }
3656        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) &&
3657                inboxMailboxId != Mailbox.NO_MAILBOX) {
3658            values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX,
3659                    uiUriString("uifolder", inboxMailboxId));
3660        }
3661        if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) {
3662            values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY);
3663        }
3664        if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) {
3665            values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI,
3666                    combinedUriString("quickresponse/account", id));
3667        }
3668        if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS)) {
3669            values.put(UIProvider.AccountColumns.SETTINGS_FRAGMENT_CLASS,
3670                    PREFERENCE_FRAGMENT_CLASS_NAME);
3671        }
3672        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
3673            values.put(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR,
3674                    mailPrefs.getDefaultReplyAll()
3675                            ? UIProvider.DefaultReplyBehavior.REPLY_ALL
3676                            : UIProvider.DefaultReplyBehavior.REPLY);
3677        }
3678        if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) {
3679            values.put(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES,
3680                    Settings.ShowImages.ASK_FIRST);
3681        }
3682
3683        final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values);
3684        sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns._ID + "=?");
3685        return sb.toString();
3686    }
3687
3688    /**
3689     * Generate a Uri string for a combined mailbox uri
3690     * @param type the uri command type (e.g. "uimessages")
3691     * @param id the id of the item (e.g. an account, mailbox, or message id)
3692     * @return a Uri string
3693     */
3694    private static String combinedUriString(String type, String id) {
3695        return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id;
3696    }
3697
3698    public static final long COMBINED_ACCOUNT_ID = 0x10000000;
3699
3700    /**
3701     * Generate an id for a combined mailbox of a given type
3702     * @param type the mailbox type for the combined mailbox
3703     * @return the id, as a String
3704     */
3705    private static String combinedMailboxId(int type) {
3706        return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type);
3707    }
3708
3709    public static long getVirtualMailboxId(long accountId, int type) {
3710        return (accountId << 32) + type;
3711    }
3712
3713    private static boolean isVirtualMailbox(long mailboxId) {
3714        return mailboxId >= 0x100000000L;
3715    }
3716
3717    private static boolean isCombinedMailbox(long mailboxId) {
3718        return (mailboxId >> 32) == COMBINED_ACCOUNT_ID;
3719    }
3720
3721    private static long getVirtualMailboxAccountId(long mailboxId) {
3722        return mailboxId >> 32;
3723    }
3724
3725    private static String getVirtualMailboxAccountIdString(long mailboxId) {
3726        return Long.toString(mailboxId >> 32);
3727    }
3728
3729    private static int getVirtualMailboxType(long mailboxId) {
3730        return (int)(mailboxId & 0xF);
3731    }
3732
3733    private void addCombinedAccountRow(MatrixCursor mc) {
3734        final long lastUsedAccountId =
3735                Preferences.getPreferences(getContext()).getLastUsedAccountId();
3736        final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId);
3737        if (id == Account.NO_ACCOUNT) return;
3738
3739        // Build a map of the requested columns to the appropriate positions
3740        final ImmutableMap.Builder<String, Integer> builder =
3741                new ImmutableMap.Builder<String, Integer>();
3742        final String[] columnNames = mc.getColumnNames();
3743        for (int i = 0; i < columnNames.length; i++) {
3744            builder.put(columnNames[i], i);
3745        }
3746        final Map<String, Integer> colPosMap = builder.build();
3747
3748        final MailPrefs mailPrefs = MailPrefs.get(getContext());
3749        final Object[] values = new Object[columnNames.length];
3750        if (colPosMap.containsKey(BaseColumns._ID)) {
3751            values[colPosMap.get(BaseColumns._ID)] = 0;
3752        }
3753        if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) {
3754            values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] =
3755                    AccountCapabilities.UNDO |
3756                    AccountCapabilities.VIRTUAL_ACCOUNT |
3757                    AccountCapabilities.CLIENT_SANITIZED_HTML;
3758        }
3759        if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) {
3760            values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] =
3761                    combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING);
3762        }
3763        if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) {
3764            values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString(
3765                    R.string.mailbox_list_account_selector_combined_view);
3766        }
3767        if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)) {
3768            values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)] =
3769                    getContext().getString(R.string.mailbox_list_account_selector_combined_view);
3770        }
3771        if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_ID)) {
3772            values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_ID)] = "Account Id";
3773        }
3774        if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) {
3775            values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown";
3776        }
3777        if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) {
3778            values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] =
3779                    "'content://" + EmailContent.AUTHORITY + "/uiundo'";
3780        }
3781        if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) {
3782            values[colPosMap.get(UIProvider.AccountColumns.URI)] =
3783                    combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING);
3784        }
3785        if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) {
3786            values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] =
3787                    EMAIL_APP_MIME_TYPE;
3788        }
3789        if (colPosMap.containsKey(UIProvider.AccountColumns.SECURITY_HOLD)) {
3790            values[colPosMap.get(UIProvider.AccountColumns.SECURITY_HOLD)] = 0;
3791        }
3792        if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)) {
3793            values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_SECURITY_URI)] = "";
3794        }
3795        if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) {
3796            values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] =
3797                    getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING);
3798        }
3799        if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) {
3800            values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] =
3801                    getExternalUriStringEmail2("compose", Long.toString(id));
3802        }
3803        if (colPosMap.containsKey(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)) {
3804            values[colPosMap.get(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)] =
3805                    uiUriString("uiacctsettings", -1);
3806        }
3807
3808        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) {
3809            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] =
3810                    Integer.toString(mailPrefs.getAutoAdvanceMode());
3811        }
3812        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) {
3813            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] =
3814                    Integer.toString(UIProvider.SnapHeaderValue.ALWAYS);
3815        }
3816        //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE)
3817        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) {
3818            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] =
3819                    Integer.toString(mailPrefs.getDefaultReplyAll()
3820                            ? UIProvider.DefaultReplyBehavior.REPLY_ALL
3821                            : UIProvider.DefaultReplyBehavior.REPLY);
3822        }
3823        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) {
3824            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] =
3825                    getConversationListIcon(mailPrefs);
3826        }
3827        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) {
3828            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] =
3829                    mailPrefs.getConfirmDelete() ? 1 : 0;
3830        }
3831        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) {
3832            values[colPosMap.get(
3833                    UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0;
3834        }
3835        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) {
3836            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] =
3837                    mailPrefs.getConfirmSend() ? 1 : 0;
3838        }
3839        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) {
3840            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] =
3841                    combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
3842        }
3843        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) {
3844            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] =
3845                    combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX));
3846        }
3847        if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)) {
3848            values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SHOW_IMAGES)] =
3849                    Settings.ShowImages.ASK_FIRST;
3850        }
3851
3852        mc.addRow(values);
3853    }
3854
3855    private static int getConversationListIcon(MailPrefs mailPrefs) {
3856        return mailPrefs.getShowSenderImages() ?
3857                UIProvider.ConversationListIcon.SENDER_IMAGE :
3858                UIProvider.ConversationListIcon.NONE;
3859    }
3860
3861    private Cursor getVirtualMailboxCursor(long mailboxId, String[] projection) {
3862        MatrixCursor mc = new MatrixCursorWithCachedColumns(projection, 1);
3863        mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId),
3864                getVirtualMailboxType(mailboxId), projection));
3865        return mc;
3866    }
3867
3868    private Object[] getVirtualMailboxRow(long accountId, int mailboxType, String[] projection) {
3869        final long id = getVirtualMailboxId(accountId, mailboxType);
3870        final String idString = Long.toString(id);
3871        Object[] values = new Object[projection.length];
3872        // Not all column values are filled in here, as some are not applicable to virtual mailboxes
3873        // The remainder are left null
3874        for (int i = 0; i < projection.length; i++) {
3875            final String column = projection[i];
3876            if (column.equals(UIProvider.FolderColumns._ID)) {
3877                values[i] = id;
3878            } else if (column.equals(UIProvider.FolderColumns.URI)) {
3879                values[i] = combinedUriString("uifolder", idString);
3880            } else if (column.equals(UIProvider.FolderColumns.NAME)) {
3881                // default empty string since all of these should use resource strings
3882                values[i] = getFolderDisplayName(getFolderTypeFromMailboxType(mailboxType), "");
3883            } else if (column.equals(UIProvider.FolderColumns.HAS_CHILDREN)) {
3884                values[i] = 0;
3885            } else if (column.equals(UIProvider.FolderColumns.CAPABILITIES)) {
3886                values[i] = UIProvider.FolderCapabilities.DELETE
3887                        | UIProvider.FolderCapabilities.IS_VIRTUAL;
3888            } else if (column.equals(UIProvider.FolderColumns.CONVERSATION_LIST_URI)) {
3889                values[i] = combinedUriString("uimessages", idString);
3890            } else if (column.equals(UIProvider.FolderColumns.UNREAD_COUNT)) {
3891                if (mailboxType == Mailbox.TYPE_INBOX && accountId == COMBINED_ACCOUNT_ID) {
3892                    final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3893                            MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID
3894                            + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE
3895                            + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0",
3896                            null);
3897                    values[i] = unreadCount;
3898                } else if (mailboxType == Mailbox.TYPE_UNREAD) {
3899                    final String accountKeyClause;
3900                    final String[] whereArgs;
3901                    if (accountId == COMBINED_ACCOUNT_ID) {
3902                        accountKeyClause = "";
3903                        whereArgs = null;
3904                    } else {
3905                        accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
3906                        whereArgs = new String[] { Long.toString(accountId) };
3907                    }
3908                    final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3909                            accountKeyClause + MessageColumns.FLAG_READ + "=0 AND "
3910                            + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns._ID
3911                            + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "="
3912                            + Mailbox.TYPE_TRASH + ")", whereArgs);
3913                    values[i] = unreadCount;
3914                } else if (mailboxType == Mailbox.TYPE_STARRED) {
3915                    final String accountKeyClause;
3916                    final String[] whereArgs;
3917                    if (accountId == COMBINED_ACCOUNT_ID) {
3918                        accountKeyClause = "";
3919                        whereArgs = null;
3920                    } else {
3921                        accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND ";
3922                        whereArgs = new String[] { Long.toString(accountId) };
3923                    }
3924                    final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI,
3925                            accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs);
3926                    values[i] = starredCount;
3927                }
3928            } else if (column.equals(UIProvider.FolderColumns.ICON_RES_ID)) {
3929                if (mailboxType == Mailbox.TYPE_INBOX) {
3930                    values[i] = R.drawable.ic_drawer_inbox_24dp;
3931                } else if (mailboxType == Mailbox.TYPE_UNREAD) {
3932                    values[i] = R.drawable.ic_drawer_unread_24dp;
3933                } else if (mailboxType == Mailbox.TYPE_STARRED) {
3934                    values[i] = R.drawable.ic_drawer_starred_24dp;
3935                }
3936            }
3937        }
3938        return values;
3939    }
3940
3941    private Cursor uiAccounts(String[] uiProjection, boolean suppressCombined) {
3942        final Context context = getContext();
3943        final SQLiteDatabase db = getDatabase(context);
3944        final Cursor accountIdCursor =
3945                db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]);
3946        final MatrixCursor mc;
3947        try {
3948            boolean combinedAccount = false;
3949            if (!suppressCombined && accountIdCursor.getCount() > 1) {
3950                combinedAccount = true;
3951            }
3952            final Bundle extras = new Bundle();
3953            // Email always returns the accurate number of accounts
3954            extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1);
3955            mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras);
3956            final Object[] values = new Object[uiProjection.length];
3957            while (accountIdCursor.moveToNext()) {
3958                final String id = accountIdCursor.getString(0);
3959                final Cursor accountCursor =
3960                        db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
3961                try {
3962                    if (accountCursor.moveToNext()) {
3963                        for (int i = 0; i < uiProjection.length; i++) {
3964                            values[i] = accountCursor.getString(i);
3965                        }
3966                        mc.addRow(values);
3967                    }
3968                } finally {
3969                    accountCursor.close();
3970                }
3971            }
3972            if (combinedAccount) {
3973                addCombinedAccountRow(mc);
3974            }
3975        } finally {
3976            accountIdCursor.close();
3977        }
3978        mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER);
3979
3980        return mc;
3981    }
3982
3983    private Cursor uiQuickResponseAccount(String[] uiProjection, String account) {
3984        final Context context = getContext();
3985        final SQLiteDatabase db = getDatabase(context);
3986        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
3987        sb.append(" FROM " + QuickResponse.TABLE_NAME);
3988        sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?");
3989        final String query = sb.toString();
3990        return db.rawQuery(query, new String[] {account});
3991    }
3992
3993    private Cursor uiQuickResponseId(String[] uiProjection, String id) {
3994        final Context context = getContext();
3995        final SQLiteDatabase db = getDatabase(context);
3996        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
3997        sb.append(" FROM " + QuickResponse.TABLE_NAME);
3998        sb.append(" WHERE " + QuickResponse._ID + "=?");
3999        final String query = sb.toString();
4000        return db.rawQuery(query, new String[] {id});
4001    }
4002
4003    private Cursor uiQuickResponse(String[] uiProjection) {
4004        final Context context = getContext();
4005        final SQLiteDatabase db = getDatabase(context);
4006        final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection);
4007        sb.append(" FROM " + QuickResponse.TABLE_NAME);
4008        final String query = sb.toString();
4009        return db.rawQuery(query, new String[0]);
4010    }
4011
4012    /**
4013     * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail
4014     *
4015     * @param uiProjection as passed from UnifiedEmail
4016     * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments
4017     * or null if there are no query parameters
4018     * @return the SQLite query to be executed on the EmailProvider database
4019     */
4020    private static String genQueryAttachments(String[] uiProjection,
4021            List<String> contentTypeQueryParameters) {
4022        // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT
4023        ContentValues values = new ContentValues(1);
4024        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
4025        StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values);
4026        sb.append(" FROM ")
4027                .append(Attachment.TABLE_NAME)
4028                .append(" WHERE ")
4029                .append(AttachmentColumns.MESSAGE_KEY)
4030                .append(" =? ");
4031
4032        // Filter for certain content types.
4033        // The filter works by adding LIKE operators for each
4034        // content type you wish to request. Content types
4035        // are filtered by performing a case-insensitive "starts with"
4036        // filter. IE, "image/" would return "image/png" as well as "image/jpeg".
4037        if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
4038            final int size = contentTypeQueryParameters.size();
4039            sb.append("AND (");
4040            for (int i = 0; i < size; i++) {
4041                final String contentType = contentTypeQueryParameters.get(i);
4042                sb.append(AttachmentColumns.MIME_TYPE)
4043                        .append(" LIKE '")
4044                        .append(contentType)
4045                        .append("%'");
4046
4047                if (i != size - 1) {
4048                    sb.append(" OR ");
4049                }
4050            }
4051            sb.append(")");
4052        }
4053        return sb.toString();
4054    }
4055
4056    /**
4057     * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail
4058     *
4059     * @param uiProjection as passed from UnifiedEmail
4060     * @return the SQLite query to be executed on the EmailProvider database
4061     */
4062    private String genQueryAttachment(String[] uiProjection) {
4063        // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS
4064        final ContentValues values = new ContentValues(2);
4065        values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL());
4066        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
4067
4068        return genSelect(getAttachmentMap(), uiProjection, values)
4069                .append(" FROM ").append(Attachment.TABLE_NAME)
4070                .append(" WHERE ")
4071                .append(AttachmentColumns._ID).append(" =? ")
4072                .toString();
4073    }
4074
4075    /**
4076     * Generate the "single attachment by Content ID" SQLite query, given a projection from
4077     * UnifiedEmail
4078     *
4079     * @param uiProjection as passed from UnifiedEmail
4080     * @return the SQLite query to be executed on the EmailProvider database
4081     */
4082    private String genQueryAttachmentByMessageIDAndCid(String[] uiProjection) {
4083        final ContentValues values = new ContentValues(2);
4084        values.put(AttachmentColumns.CONTENT_URI, createAttachmentUriColumnSQL());
4085        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1);
4086
4087        return genSelect(getAttachmentMap(), uiProjection, values)
4088                .append(" FROM ").append(Attachment.TABLE_NAME)
4089                .append(" WHERE ")
4090                .append(AttachmentColumns.MESSAGE_KEY).append(" =? ")
4091                .append(" AND ")
4092                .append(AttachmentColumns.CONTENT_ID).append(" =? ")
4093                .toString();
4094    }
4095
4096    /**
4097     * @return a fragment of SQL that is the expression which, when evaluated for a particular
4098     *      Attachment row, produces the Content URI for the attachment
4099     */
4100    private static String createAttachmentUriColumnSQL() {
4101        final String uriPrefix = Attachment.ATTACHMENT_PROVIDER_URI_PREFIX;
4102        final String accountKey = AttachmentColumns.ACCOUNT_KEY;
4103        final String id = AttachmentColumns._ID;
4104        final String raw = AttachmentUtilities.FORMAT_RAW;
4105        final String contentUri = String.format("%s/' || %s || '/' || %s || '/%s", uriPrefix,
4106                accountKey, id, raw);
4107
4108        return "@CASE " +
4109                "WHEN contentUri IS NULL THEN '" + contentUri + "' " +
4110                "WHEN contentUri IS NOT NULL THEN contentUri " +
4111                "END";
4112    }
4113
4114    /**
4115     * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail
4116     *
4117     * @param uiProjection as passed from UnifiedEmail
4118     * @return the SQLite query to be executed on the EmailProvider database
4119     */
4120    private static String genQuerySubfolders(String[] uiProjection) {
4121        StringBuilder sb = genSelect(getFolderListMap(), uiProjection);
4122        sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY +
4123                " =? ORDER BY ");
4124        sb.append(MAILBOX_ORDER_BY);
4125        return sb.toString();
4126    }
4127
4128    private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID);
4129
4130    /**
4131     * Returns a cursor over all the folders for a specific URI which corresponds to a single
4132     * account.
4133     * @param uri uri to query
4134     * @param uiProjection projection
4135     * @return query result cursor
4136     */
4137    private Cursor uiFolders(final Uri uri, final String[] uiProjection) {
4138        final Context context = getContext();
4139        final SQLiteDatabase db = getDatabase(context);
4140        final String id = uri.getPathSegments().get(1);
4141
4142        final Uri notifyUri =
4143                UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
4144
4145        final Cursor vc = uiVirtualMailboxes(id, uiProjection);
4146        vc.setNotificationUri(context.getContentResolver(), notifyUri);
4147        if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
4148            return vc;
4149        } else {
4150            Cursor c = db.rawQuery(genQueryAccountMailboxes(UIProvider.FOLDERS_PROJECTION),
4151                    new String[] {id});
4152            c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
4153            c.setNotificationUri(context.getContentResolver(), notifyUri);
4154            if (c.getCount() > 0) {
4155                Cursor[] cursors = new Cursor[]{vc, c};
4156                return new MergeCursor(cursors);
4157            } else {
4158                return c;
4159            }
4160        }
4161    }
4162
4163    private Cursor uiVirtualMailboxes(final String id, final String[] uiProjection) {
4164        final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
4165
4166        if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
4167            mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX, uiProjection));
4168            mc.addRow(
4169                    getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED, uiProjection));
4170            mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD, uiProjection));
4171        } else {
4172            final long acctId = Long.parseLong(id);
4173            mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED, uiProjection));
4174            mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD, uiProjection));
4175        }
4176
4177        return mc;
4178    }
4179
4180    /**
4181     * Returns an array of the default recent folders for a given URI which is unique for an
4182     * account. Some accounts might not have default recent folders, in which case an empty array
4183     * is returned.
4184     * @param id account id
4185     * @return array of URIs
4186     */
4187    private Uri[] defaultRecentFolders(final String id) {
4188        Uri[] recentFolders = new Uri[0];
4189        final SQLiteDatabase db = getDatabase(getContext());
4190        if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
4191            // We don't have default recents for the combined view.
4192            return recentFolders;
4193        }
4194        // We search for the types we want, and find corresponding IDs.
4195        final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE };
4196
4197        // Sent, Drafts, and Starred are the default recents.
4198        final StringBuilder sb = genSelect(getFolderListMap(), idAndType);
4199        sb.append(" FROM ")
4200                .append(Mailbox.TABLE_NAME)
4201                .append(" WHERE ")
4202                .append(MailboxColumns.ACCOUNT_KEY)
4203                .append(" = ")
4204                .append(id)
4205                .append(" AND ")
4206                .append(MailboxColumns.TYPE)
4207                .append(" IN (")
4208                .append(Mailbox.TYPE_SENT)
4209                .append(", ")
4210                .append(Mailbox.TYPE_DRAFTS)
4211                .append(", ")
4212                .append(Mailbox.TYPE_STARRED)
4213                .append(")");
4214        LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb);
4215        final Cursor c = db.rawQuery(sb.toString(), null);
4216        try {
4217            if (c == null || c.getCount() <= 0 || !c.moveToFirst()) {
4218                return recentFolders;
4219            }
4220            // Read all the IDs of the mailboxes, and turn them into URIs.
4221            recentFolders = new Uri[c.getCount()];
4222            int i = 0;
4223            do {
4224                final long folderId = c.getLong(0);
4225                recentFolders[i] = uiUri("uifolder", folderId);
4226                LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId,
4227                        recentFolders[i]);
4228                ++i;
4229            } while (c.moveToNext());
4230        } finally {
4231            if (c != null) {
4232                c.close();
4233            }
4234        }
4235        return recentFolders;
4236    }
4237
4238    /**
4239     * Convenience method to create a {@link Folder}
4240     * @param context to get a {@link ContentResolver}
4241     * @param mailboxId id of the {@link Mailbox} that we want
4242     * @return the {@link Folder} or null
4243     */
4244    public static Folder getFolder(Context context, long mailboxId) {
4245        final ContentResolver resolver = context.getContentResolver();
4246        final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId),
4247                UIProvider.FOLDERS_PROJECTION, null, null, null);
4248
4249        if (fc == null) {
4250            LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId);
4251            return null;
4252        }
4253
4254        Folder uiFolder = null;
4255        try {
4256            if (fc.moveToFirst()) {
4257                uiFolder = new Folder(fc);
4258            }
4259        } finally {
4260            fc.close();
4261        }
4262        return uiFolder;
4263    }
4264
4265    static class AttachmentsCursor extends CursorWrapper {
4266        private final int mContentUriIndex;
4267        private final int mUriIndex;
4268        private final Context mContext;
4269        private final String[] mContentUriStrings;
4270
4271        public AttachmentsCursor(Context context, Cursor cursor) {
4272            super(cursor);
4273            mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI);
4274            mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI);
4275            mContext = context;
4276            mContentUriStrings = new String[cursor.getCount()];
4277            if (mContentUriIndex == -1) {
4278                // Nothing to do here, move along
4279                return;
4280            }
4281            while (cursor.moveToNext()) {
4282                final int index = cursor.getPosition();
4283                final Uri uri = Uri.parse(getString(mUriIndex));
4284                final long id = Long.parseLong(uri.getLastPathSegment());
4285                final Attachment att = Attachment.restoreAttachmentWithId(mContext, id);
4286
4287                if (att == null) {
4288                    mContentUriStrings[index] = "";
4289                    continue;
4290                }
4291
4292                if (!TextUtils.isEmpty(att.getCachedFileUri())) {
4293                    mContentUriStrings[index] = att.getCachedFileUri();
4294                    continue;
4295                }
4296
4297                final String contentUri;
4298                // Until the package installer can handle opening apks from a content:// uri, for
4299                // any apk that was successfully saved in external storage, return the
4300                // content uri from the attachment
4301                if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL &&
4302                        att.mUiState == UIProvider.AttachmentState.SAVED &&
4303                        TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) {
4304                    contentUri = att.getContentUri();
4305                } else {
4306                    final String attUriString = att.getContentUri();
4307                    final String authority;
4308                    if (!TextUtils.isEmpty(attUriString)) {
4309                        authority = Uri.parse(attUriString).getAuthority();
4310                    } else {
4311                        authority = null;
4312                    }
4313                    if (TextUtils.equals(authority, Attachment.ATTACHMENT_PROVIDER_AUTHORITY)) {
4314                        contentUri = attUriString;
4315                    } else {
4316                        contentUri = AttachmentUtilities.getAttachmentUri(att.mAccountKey, id)
4317                                .toString();
4318                    }
4319                }
4320                mContentUriStrings[index] = contentUri;
4321
4322            }
4323            cursor.moveToPosition(-1);
4324        }
4325
4326        @Override
4327        public String getString(int column) {
4328            if (column == mContentUriIndex) {
4329                return mContentUriStrings[getPosition()];
4330            } else {
4331                return super.getString(column);
4332            }
4333        }
4334    }
4335
4336    /**
4337     * For debugging purposes; shouldn't be used in production code
4338     */
4339    @SuppressWarnings("unused")
4340    static class CloseDetectingCursor extends CursorWrapper {
4341
4342        public CloseDetectingCursor(Cursor cursor) {
4343            super(cursor);
4344        }
4345
4346        @Override
4347        public void close() {
4348            super.close();
4349            LogUtils.d(TAG, "Closing cursor", new Error());
4350        }
4351    }
4352
4353    /**
4354     * Converts a mailbox in a row of the mailboxCursor into a row
4355     * in the supplied {@link MatrixCursor} in the format required for {@link Folder}.
4356     * As a convenience, the modified {@link MatrixCursor} is also returned.
4357     * @param mc the {@link MatrixCursor} into which the mailbox data will be converted
4358     * @param projectionLength the length of the projection for this Cursor
4359     * @param mailboxCursor the cursor supplying the mailbox data
4360     * @param nameColumn column in the cursor containing the folder name value
4361     * @param typeColumn column in the cursor containing the folder type value
4362     * @return the {@link MatrixCursor} containing the transformed data.
4363     */
4364    private Cursor getUiFolderCursorRowFromMailboxCursorRow(
4365            MatrixCursor mc, int projectionLength, Cursor mailboxCursor,
4366            int nameColumn, int typeColumn) {
4367        final MatrixCursor.RowBuilder builder = mc.newRow();
4368        for (int i = 0; i < projectionLength; i++) {
4369            // If we are at the name column, get the type
4370            // and use it to use a properly translated string
4371            // from resources instead of the display name.
4372            // This ignores display names for system mailboxes.
4373            if (nameColumn == i) {
4374                // We implicitly assume that if name is requested,
4375                // type has also been requested. If not, this will
4376                // error in unknown ways.
4377                final int type = mailboxCursor.getInt(typeColumn);
4378                builder.add(getFolderDisplayName(type, mailboxCursor.getString(i)));
4379            } else {
4380                builder.add(mailboxCursor.getString(i));
4381            }
4382        }
4383        return mc;
4384    }
4385
4386    /**
4387     * Takes a uifolder cursor (that was generated with a full projection) and remaps values for
4388     * columns that are difficult to generate in the SQL query. This currently includes:
4389     * - Folder name (due to system folder localization).
4390     * - Capabilities (due to this varying by account protocol).
4391     * - Persistent id (due to needing to base64 encode it).
4392     * - Load more uri (due to this varying by account protocol).
4393     * TODO: This would be better as a CursorWrapper, rather than doing a copy.
4394     * @param inputCursor A cursor containing all columns of {@link UIProvider.FolderColumns}.
4395     *                    Strictly speaking doesn't need all, but simpler if we assume that.
4396     * @param outputCursor A MatrixCursor which this function will populate.
4397     * @param accountId The account id for the mailboxes in this query.
4398     * @param uiProjection The projection specified by the query.
4399     */
4400    private void remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor,
4401            final long accountId, final String[] uiProjection) {
4402        // Return early if our input cursor is empty.
4403        if (inputCursor == null || inputCursor.getCount() == 0) {
4404            return;
4405        }
4406        // Get the column indices for the columns we need during remapping.
4407        // While we currently could assume the column indices for UIProvider.FOLDERS_PROJECTION
4408        // and therefore avoid the calls to getColumnIndex, this at least tries to future-proof a
4409        // bit.
4410        // Note that id and type MUST be present for this function to work correctly.
4411        final int idColumn = inputCursor.getColumnIndex(BaseColumns._ID);
4412        final int typeColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.TYPE);
4413        final int nameColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.NAME);
4414        final int capabilitiesColumn =
4415                inputCursor.getColumnIndex(UIProvider.FolderColumns.CAPABILITIES);
4416        final int persistentIdColumn =
4417                inputCursor.getColumnIndex(UIProvider.FolderColumns.PERSISTENT_ID);
4418        final int loadMoreUriColumn =
4419                inputCursor.getColumnIndex(UIProvider.FolderColumns.LOAD_MORE_URI);
4420
4421        // Get the EmailServiceInfo for the current account.
4422        final Context context = getContext();
4423        final String protocol = Account.getProtocol(context, accountId);
4424        final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
4425
4426        // Build the return cursor. We iterate over all rows of the input cursor and construct
4427        // a row in the output using the columns in uiProjection.
4428        while (inputCursor.moveToNext()) {
4429            final MatrixCursor.RowBuilder builder = outputCursor.newRow();
4430            final int folderType = inputCursor.getInt(typeColumn);
4431            for (int i = 0; i < uiProjection.length; i++) {
4432                // Find the index in the input cursor corresponding the column requested in the
4433                // output projection.
4434                final int index = inputCursor.getColumnIndex(uiProjection[i]);
4435                if (index == -1) {
4436                    // We don't have this value, so put a blank in the output and move on.
4437                    builder.add(null);
4438                    continue;
4439                }
4440                final String value = inputCursor.getString(index);
4441                // remapped indicates whether we've written a value to the output for this column.
4442                final boolean remapped;
4443                if (nameColumn == index) {
4444                    // Remap folder name for system folders.
4445                    builder.add(getFolderDisplayName(folderType, value));
4446                    remapped = true;
4447                } else if (capabilitiesColumn == index) {
4448                    // Get the correct capabilities for this folder.
4449                    final long mailboxID = inputCursor.getLong(idColumn);
4450                    final int mailboxType = getMailboxTypeFromFolderType(folderType);
4451                    builder.add(getFolderCapabilities(info, mailboxType, mailboxID));
4452                    remapped = true;
4453                } else if (persistentIdColumn == index) {
4454                    // Hash the persistent id.
4455                    builder.add(Base64.encodeToString(value.getBytes(),
4456                            Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
4457                    remapped = true;
4458                } else if (loadMoreUriColumn == index && folderType != Mailbox.TYPE_SEARCH &&
4459                        (info == null || !info.offerLoadMore)) {
4460                    // Blank the load more uri for account types that don't offer it.
4461                    // Note that all account types permit load more for search results.
4462                    builder.add(null);
4463                    remapped = true;
4464                } else {
4465                    remapped = false;
4466                }
4467                // If the above logic didn't write some other value to the output, use the value
4468                // from the input cursor.
4469                if (!remapped) {
4470                    builder.add(value);
4471                }
4472            }
4473        }
4474    }
4475
4476    private Cursor getFolderListCursor(final Cursor inputCursor, final long accountId,
4477            final String[] uiProjection) {
4478        final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection);
4479        if (inputCursor != null) {
4480            try {
4481                remapFolderCursor(inputCursor, mc, accountId, uiProjection);
4482            } finally {
4483                inputCursor.close();
4484            }
4485        }
4486        return mc;
4487    }
4488
4489    /**
4490     * Returns a {@link String} from Resources corresponding
4491     * to the {@link UIProvider.FolderType} requested.
4492     * @param folderType {@link UIProvider.FolderType} value for the folder
4493     * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType}
4494     *                    provided is not a system folder.
4495     * @return a {@link String} to use as the display name for the folder
4496     */
4497    private String getFolderDisplayName(int folderType, String defaultName) {
4498        final int resId;
4499        switch (folderType) {
4500            case UIProvider.FolderType.INBOX:
4501                resId = R.string.mailbox_name_display_inbox;
4502                break;
4503            case UIProvider.FolderType.OUTBOX:
4504                resId = R.string.mailbox_name_display_outbox;
4505                break;
4506            case UIProvider.FolderType.DRAFT:
4507                resId = R.string.mailbox_name_display_drafts;
4508                break;
4509            case UIProvider.FolderType.TRASH:
4510                resId = R.string.mailbox_name_display_trash;
4511                break;
4512            case UIProvider.FolderType.SENT:
4513                resId = R.string.mailbox_name_display_sent;
4514                break;
4515            case UIProvider.FolderType.SPAM:
4516                resId = R.string.mailbox_name_display_junk;
4517                break;
4518            case UIProvider.FolderType.STARRED:
4519                resId = R.string.mailbox_name_display_starred;
4520                break;
4521            case UIProvider.FolderType.UNREAD:
4522                resId = R.string.mailbox_name_display_unread;
4523                break;
4524            default:
4525                return defaultName;
4526        }
4527        return getContext().getString(resId);
4528    }
4529
4530    /**
4531     * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType}
4532     * equivalent.
4533     * @param mailboxType a {@link Mailbox} type
4534     * @return a {@link UIProvider.FolderType} value
4535     */
4536    private static int getFolderTypeFromMailboxType(int mailboxType) {
4537        switch (mailboxType) {
4538            case Mailbox.TYPE_INBOX:
4539                return UIProvider.FolderType.INBOX;
4540            case Mailbox.TYPE_OUTBOX:
4541                return UIProvider.FolderType.OUTBOX;
4542            case Mailbox.TYPE_DRAFTS:
4543                return UIProvider.FolderType.DRAFT;
4544            case Mailbox.TYPE_TRASH:
4545                return UIProvider.FolderType.TRASH;
4546            case Mailbox.TYPE_SENT:
4547                return UIProvider.FolderType.SENT;
4548            case Mailbox.TYPE_JUNK:
4549                return UIProvider.FolderType.SPAM;
4550            case Mailbox.TYPE_STARRED:
4551                return UIProvider.FolderType.STARRED;
4552            case Mailbox.TYPE_UNREAD:
4553                return UIProvider.FolderType.UNREAD;
4554            case Mailbox.TYPE_SEARCH:
4555                // TODO Can the DEFAULT type be removed from SEARCH folders?
4556                return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH;
4557            default:
4558                return UIProvider.FolderType.DEFAULT;
4559        }
4560    }
4561
4562    /**
4563     * Converts a {@link UIProvider.FolderType} type value to its {@link Mailbox} equivalent.
4564     * @param folderType a {@link UIProvider.FolderType} type
4565     * @return a {@link Mailbox} value
4566     */
4567    private static int getMailboxTypeFromFolderType(int folderType) {
4568        switch (folderType) {
4569            case UIProvider.FolderType.DEFAULT:
4570                return Mailbox.TYPE_MAIL;
4571            case UIProvider.FolderType.INBOX:
4572                return Mailbox.TYPE_INBOX;
4573            case UIProvider.FolderType.OUTBOX:
4574                return Mailbox.TYPE_OUTBOX;
4575            case UIProvider.FolderType.DRAFT:
4576                return Mailbox.TYPE_DRAFTS;
4577            case UIProvider.FolderType.TRASH:
4578                return Mailbox.TYPE_TRASH;
4579            case UIProvider.FolderType.SENT:
4580                return Mailbox.TYPE_SENT;
4581            case UIProvider.FolderType.SPAM:
4582                return Mailbox.TYPE_JUNK;
4583            case UIProvider.FolderType.STARRED:
4584                return Mailbox.TYPE_STARRED;
4585            case UIProvider.FolderType.UNREAD:
4586                return Mailbox.TYPE_UNREAD;
4587            case UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH:
4588                // TODO Can the DEFAULT type be removed from SEARCH folders?
4589                return Mailbox.TYPE_SEARCH;
4590            default:
4591                throw new IllegalArgumentException("Unable to map folder type: " + folderType);
4592        }
4593    }
4594
4595    /**
4596     * We need a reasonably full projection for getFolderListCursor to work, but don't always want
4597     * to do the subquery needed for FolderColumns.UNREAD_SENDERS
4598     * @param uiProjection The projection we actually want
4599     * @return Full projection, possibly with or without FolderColumns.UNREAD_SENDERS
4600     */
4601    private String[] folderProjectionFromUiProjection(final String[] uiProjection) {
4602        final Set<String> columns = ImmutableSet.copyOf(uiProjection);
4603        if (columns.contains(UIProvider.FolderColumns.UNREAD_SENDERS)) {
4604            return UIProvider.FOLDERS_PROJECTION_WITH_UNREAD_SENDERS;
4605        } else {
4606            return UIProvider.FOLDERS_PROJECTION;
4607        }
4608    }
4609
4610    /**
4611     * Handle UnifiedEmail queries here (dispatched from query())
4612     *
4613     * @param match the UriMatcher match for the original uri passed in from UnifiedEmail
4614     * @param uri the original uri passed in from UnifiedEmail
4615     * @param uiProjection the projection passed in from UnifiedEmail
4616     * @param unseenOnly <code>true</code> to only return unseen messages (where supported)
4617     * @return the result Cursor
4618     */
4619    private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) {
4620        Context context = getContext();
4621        ContentResolver resolver = context.getContentResolver();
4622        SQLiteDatabase db = getDatabase(context);
4623        // Should we ever return null, or throw an exception??
4624        Cursor c = null;
4625        String id = uri.getPathSegments().get(1);
4626        Uri notifyUri = null;
4627        switch(match) {
4628            case UI_ALL_FOLDERS:
4629                notifyUri =
4630                        UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
4631                final Cursor vc = uiVirtualMailboxes(id, uiProjection);
4632                if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
4633                    // There's no real mailboxes, so just return the virtual ones
4634                    c = vc;
4635                } else {
4636                    // Return real and virtual mailboxes alike
4637                    final Cursor rawc = db.rawQuery(genQueryAccountAllMailboxes(uiProjection),
4638                            new String[] {id});
4639                    rawc.setNotificationUri(context.getContentResolver(), notifyUri);
4640                    vc.setNotificationUri(context.getContentResolver(), notifyUri);
4641                    if (rawc.getCount() > 0) {
4642                        c = new MergeCursor(new Cursor[]{rawc, vc});
4643                    } else {
4644                        c = rawc;
4645                    }
4646                }
4647                break;
4648            case UI_FULL_FOLDERS: {
4649                // We need a full projection for getFolderListCursor
4650                final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
4651                c = db.rawQuery(genQueryAccountAllMailboxes(folderProjection), new String[] {id});
4652                c = getFolderListCursor(c, Long.valueOf(id), uiProjection);
4653                notifyUri =
4654                        UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build();
4655                break;
4656            }
4657            case UI_RECENT_FOLDERS:
4658                c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id});
4659                notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
4660                break;
4661            case UI_SUBFOLDERS: {
4662                // We need a full projection for getFolderListCursor
4663                final String[] folderProjection = folderProjectionFromUiProjection(uiProjection);
4664                c = db.rawQuery(genQuerySubfolders(folderProjection), new String[] {id});
4665                c = getFolderListCursor(c, Mailbox.getAccountIdForMailbox(context, id),
4666                        uiProjection);
4667                // Get notifications for any folder changes on this account. This is broader than
4668                // we need but otherwise we'd need for every folder change to notify on all relevant
4669                // subtrees. For now we opt for simplicity.
4670                final long accountId = Mailbox.getAccountIdForMailbox(context, id);
4671                notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
4672                break;
4673            }
4674            case UI_MESSAGES:
4675                long mailboxId = Long.parseLong(id);
4676                final Folder folder = getFolder(context, mailboxId);
4677                if (folder == null) {
4678                    // This mailboxId is bogus. Return an empty cursor
4679                    // TODO: Make callers of this query handle null cursors instead b/10819309
4680                    return new MatrixCursor(uiProjection);
4681                }
4682                if (isVirtualMailbox(mailboxId)) {
4683                    c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly);
4684                } else {
4685                    c = db.rawQuery(
4686                            genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id});
4687                }
4688                notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build();
4689                c = new EmailConversationCursor(context, c, folder, mailboxId);
4690                break;
4691            case UI_MESSAGE:
4692                MessageQuery qq = genQueryViewMessage(uiProjection, id);
4693                String sql = qq.query;
4694                String attJson = qq.attachmentJson;
4695                // With attachments, we have another argument to bind
4696                if (attJson != null) {
4697                    c = db.rawQuery(sql, new String[] {attJson, id});
4698                } else {
4699                    c = db.rawQuery(sql, new String[] {id});
4700                }
4701                if (c != null) {
4702                    c = new EmailMessageCursor(getContext(), c, UIProvider.MessageColumns.BODY_HTML,
4703                            UIProvider.MessageColumns.BODY_TEXT);
4704                }
4705                notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();
4706                break;
4707            case UI_ATTACHMENTS:
4708                final List<String> contentTypeQueryParameters =
4709                        uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
4710                c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters),
4711                        new String[] {id});
4712                c = new AttachmentsCursor(context, c);
4713                notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
4714                break;
4715            case UI_ATTACHMENT:
4716                c = db.rawQuery(genQueryAttachment(uiProjection), new String[] {id});
4717                notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build();
4718                break;
4719            case UI_ATTACHMENT_BY_CID:
4720                final String cid = uri.getPathSegments().get(2);
4721                final String[] selectionArgs = {id, cid};
4722                c = db.rawQuery(genQueryAttachmentByMessageIDAndCid(uiProjection), selectionArgs);
4723
4724                // we don't have easy access to the attachment ID (which is buried in the cursor
4725                // being returned), so we notify on the parent message object
4726                notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build();
4727                break;
4728            case UI_FOLDER:
4729            case UI_INBOX:
4730                if (match == UI_INBOX) {
4731                    mailboxId = Mailbox.findMailboxOfType(context, Long.parseLong(id),
4732                            Mailbox.TYPE_INBOX);
4733                    if (mailboxId == Mailbox.NO_MAILBOX) {
4734                        LogUtils.d(LogUtils.TAG, "No inbox found for account %s", id);
4735                        return null;
4736                    }
4737                    LogUtils.d(LogUtils.TAG, "Found inbox id %d", mailboxId);
4738                } else {
4739                    mailboxId = Long.parseLong(id);
4740                }
4741                final String mailboxIdString = Long.toString(mailboxId);
4742                if (isVirtualMailbox(mailboxId)) {
4743                    c = getVirtualMailboxCursor(mailboxId, uiProjection);
4744                    notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString)
4745                            .build();
4746                } else {
4747                    c = db.rawQuery(genQueryMailbox(uiProjection, mailboxIdString),
4748                            new String[]{mailboxIdString});
4749                    final List<String> projectionList = Arrays.asList(uiProjection);
4750                    final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME);
4751                    final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE);
4752                    if (c.moveToFirst()) {
4753                        final Cursor closeThis = c;
4754                        try {
4755                            c = getUiFolderCursorRowFromMailboxCursorRow(
4756                                    new MatrixCursorWithCachedColumns(uiProjection),
4757                                    uiProjection.length, c, nameColumn, typeColumn);
4758                        } finally {
4759                            closeThis.close();
4760                        }
4761                    }
4762                    notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(mailboxIdString)
4763                            .build();
4764                }
4765                break;
4766            case UI_ACCOUNT:
4767                if (id.equals(COMBINED_ACCOUNT_ID_STRING)) {
4768                    MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1);
4769                    addCombinedAccountRow(mc);
4770                    c = mc;
4771                } else {
4772                    c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id});
4773                }
4774                notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build();
4775                break;
4776            case UI_CONVERSATION:
4777                c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id});
4778                break;
4779        }
4780        if (notifyUri != null) {
4781            c.setNotificationUri(resolver, notifyUri);
4782        }
4783        return c;
4784    }
4785
4786    /**
4787     * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need
4788     * a few of the fields
4789     * @param uiAtt the UIProvider attachment to convert
4790     * @param cachedFile the path to the cached file to
4791     * @return the EmailProvider attachment
4792     */
4793    // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be
4794    // removed
4795    // TODO(mhibdon): if the UI Attachment contained the account key, the third parameter could
4796    // be removed.
4797    private static Attachment convertUiAttachmentToAttachment(
4798            com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey) {
4799        final Attachment att = new Attachment();
4800
4801        att.setContentUri(uiAtt.contentUri.toString());
4802
4803        if (!TextUtils.isEmpty(cachedFile)) {
4804            // Generate the content provider uri for this cached file
4805            final Uri.Builder cachedFileBuilder = Uri.parse(
4806                    "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon();
4807            cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile);
4808            att.setCachedFileUri(cachedFileBuilder.build().toString());
4809        }
4810        att.mAccountKey = accountKey;
4811        att.mFileName = uiAtt.getName();
4812        att.mMimeType = uiAtt.getContentType();
4813        att.mSize = uiAtt.size;
4814        return att;
4815    }
4816
4817    /**
4818     * Create a mailbox given the account and mailboxType.
4819     */
4820    private Mailbox createMailbox(long accountId, int mailboxType) {
4821        Context context = getContext();
4822        Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType);
4823        // Make sure drafts and save will show up in recents...
4824        // If these already exist (from old Email app), they will have touch times
4825        switch (mailboxType) {
4826            case Mailbox.TYPE_DRAFTS:
4827                box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME;
4828                break;
4829            case Mailbox.TYPE_SENT:
4830                box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME;
4831                break;
4832        }
4833        box.save(context);
4834        return box;
4835    }
4836
4837    /**
4838     * Given an account name and a mailbox type, return that mailbox, creating it if necessary
4839     * @param accountId the account id to use
4840     * @param mailboxType the type of mailbox we're trying to find
4841     * @return the mailbox of the given type for the account in the uri, or null if not found
4842     */
4843    private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) {
4844        Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType);
4845        if (mailbox == null) {
4846            mailbox = createMailbox(accountId, mailboxType);
4847        }
4848        return mailbox;
4849    }
4850
4851    /**
4852     * Given a mailbox and the content values for a message, create/save the message in the mailbox
4853     * @param mailbox the mailbox to use
4854     * @param extras the bundle containing the message fields
4855     * @return the uri of the newly created message
4856     * TODO(yph): The following fields are available in extras but unused, verify whether they
4857     *     should be respected:
4858     *     - UIProvider.MessageColumns.SNIPPET
4859     *     - UIProvider.MessageColumns.REPLY_TO
4860     *     - UIProvider.MessageColumns.FROM
4861     */
4862    private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) {
4863        final Context context = getContext();
4864        // Fill in the message
4865        final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
4866        if (account == null) return null;
4867        final String customFromAddress =
4868                extras.getString(UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS);
4869        if (!TextUtils.isEmpty(customFromAddress)) {
4870            msg.mFrom = customFromAddress;
4871        } else {
4872            msg.mFrom = account.getEmailAddress();
4873        }
4874        msg.mTimeStamp = System.currentTimeMillis();
4875        msg.mTo = extras.getString(UIProvider.MessageColumns.TO);
4876        msg.mCc = extras.getString(UIProvider.MessageColumns.CC);
4877        msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC);
4878        msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT);
4879        msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT);
4880        msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML);
4881        msg.mMailboxKey = mailbox.mId;
4882        msg.mAccountKey = mailbox.mAccountKey;
4883        msg.mDisplayName = msg.mTo;
4884        msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
4885        msg.mFlagRead = true;
4886        msg.mFlagSeen = true;
4887        msg.mQuotedTextStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS, 0);
4888        int flags = 0;
4889        final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE);
4890        switch(draftType) {
4891            case DraftType.FORWARD:
4892                flags |= Message.FLAG_TYPE_FORWARD;
4893                break;
4894            case DraftType.REPLY_ALL:
4895                flags |= Message.FLAG_TYPE_REPLY_ALL;
4896                //$FALL-THROUGH$
4897            case DraftType.REPLY:
4898                flags |= Message.FLAG_TYPE_REPLY;
4899                break;
4900            case DraftType.COMPOSE:
4901                flags |= Message.FLAG_TYPE_ORIGINAL;
4902                break;
4903        }
4904        int draftInfo = 0;
4905        if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) {
4906            draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS);
4907            if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) {
4908                draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE;
4909            }
4910        }
4911        if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) {
4912            flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT;
4913        }
4914        msg.mDraftInfo = draftInfo;
4915        msg.mFlags = flags;
4916
4917        final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID);
4918        if (ref != null && msg.mQuotedTextStartPos >= 0) {
4919            String refId = Uri.parse(ref).getLastPathSegment();
4920            try {
4921                msg.mSourceKey = Long.parseLong(refId);
4922            } catch (NumberFormatException e) {
4923                // This will be zero; the default
4924            }
4925        }
4926
4927        // Get attachments from the ContentValues
4928        final List<com.android.mail.providers.Attachment> uiAtts =
4929                com.android.mail.providers.Attachment.fromJSONArray(
4930                        extras.getString(UIProvider.MessageColumns.ATTACHMENTS));
4931        final ArrayList<Attachment> atts = new ArrayList<Attachment>();
4932        boolean hasUnloadedAttachments = false;
4933        Bundle attachmentFds =
4934                extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP);
4935        for (com.android.mail.providers.Attachment uiAtt: uiAtts) {
4936            final Uri attUri = uiAtt.uri;
4937            if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) {
4938                // If it's one of ours, retrieve the attachment and add it to the list
4939                final long attId = Long.parseLong(attUri.getLastPathSegment());
4940                final Attachment att = Attachment.restoreAttachmentWithId(context, attId);
4941                if (att != null) {
4942                    // We must clone the attachment into a new one for this message; easiest to
4943                    // use a parcel here
4944                    final Parcel p = Parcel.obtain();
4945                    att.writeToParcel(p, 0);
4946                    p.setDataPosition(0);
4947                    final Attachment attClone = new Attachment(p);
4948                    p.recycle();
4949                    // Clear the messageKey (this is going to be a new attachment)
4950                    attClone.mMessageKey = 0;
4951                    // If we're sending this, it's not loaded, and we're not smart forwarding
4952                    // add the download flag, so that ADS will start up
4953                    if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null &&
4954                            ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
4955                        attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD;
4956                        hasUnloadedAttachments = true;
4957                    }
4958                    atts.add(attClone);
4959                }
4960            } else {
4961                // Cache the attachment.  This will allow us to send it, if the permissions are
4962                // revoked.
4963                final String cachedFileUri =
4964                        AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds);
4965
4966                // Convert external attachment to one of ours and add to the list
4967                atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri, msg.mAccountKey));
4968            }
4969        }
4970        if (!atts.isEmpty()) {
4971            msg.mAttachments = atts;
4972            msg.mFlagAttachment = true;
4973            if (hasUnloadedAttachments) {
4974                Utility.showToast(context, R.string.message_view_attachment_background_load);
4975            }
4976        }
4977        // Save it or update it...
4978        if (!msg.isSaved()) {
4979            msg.save(context);
4980        } else {
4981            // This is tricky due to how messages/attachments are saved; rather than putz with
4982            // what's changed, we'll delete/re-add them
4983            final ArrayList<ContentProviderOperation> ops =
4984                    new ArrayList<ContentProviderOperation>();
4985            // Delete all existing attachments
4986            ops.add(ContentProviderOperation.newDelete(
4987                    ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId))
4988                    .build());
4989            // Delete the body
4990            ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI)
4991                    .withSelection(BodyColumns.MESSAGE_KEY + "=?",
4992                            new String[] {Long.toString(msg.mId)})
4993                    .build());
4994            // Add the ops for the message, atts, and body
4995            msg.addSaveOps(ops);
4996            // Do it!
4997            try {
4998                applyBatch(ops);
4999            } catch (OperationApplicationException e) {
5000                LogUtils.d(TAG, "applyBatch exception");
5001            }
5002        }
5003        notifyUIMessage(msg.mId);
5004
5005        if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
5006            startSync(mailbox, 0);
5007            final long originalMsgId = msg.mSourceKey;
5008            if (originalMsgId != 0) {
5009                final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId);
5010                // If the original message exists, set its forwarded/replied to flags
5011                if (originalMsg != null) {
5012                    final ContentValues cv = new ContentValues();
5013                    flags = originalMsg.mFlags;
5014                    switch(draftType) {
5015                        case DraftType.FORWARD:
5016                            flags |= Message.FLAG_FORWARDED;
5017                            break;
5018                        case DraftType.REPLY_ALL:
5019                        case DraftType.REPLY:
5020                            flags |= Message.FLAG_REPLIED_TO;
5021                            break;
5022                    }
5023                    cv.put(MessageColumns.FLAGS, flags);
5024                    context.getContentResolver().update(ContentUris.withAppendedId(
5025                            Message.CONTENT_URI, originalMsgId), cv, null, null);
5026                }
5027            }
5028        }
5029        return uiUri("uimessage", msg.mId);
5030    }
5031
5032    private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) {
5033        final Mailbox mailbox =
5034                getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS);
5035        if (mailbox == null) return null;
5036        Message msg = null;
5037        if (extras.containsKey(BaseColumns._ID)) {
5038            final long messageId = extras.getLong(BaseColumns._ID);
5039            msg = Message.restoreMessageWithId(getContext(), messageId);
5040        }
5041        if (msg == null) {
5042            msg = new Message();
5043        }
5044        return uiSaveMessage(msg, mailbox, extras);
5045    }
5046
5047    private Uri uiSendDraftMessage(final long accountId, final Bundle extras) {
5048        final Message msg;
5049        if (extras.containsKey(BaseColumns._ID)) {
5050            final long messageId = extras.getLong(BaseColumns._ID);
5051            msg = Message.restoreMessageWithId(getContext(), messageId);
5052        } else {
5053            msg = new Message();
5054        }
5055
5056        if (msg == null) return null;
5057        final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX);
5058        if (mailbox == null) return null;
5059        // Make sure the sent mailbox exists, since it will be necessary soon.
5060        // TODO(yph): move system mailbox creation to somewhere sane.
5061        final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT);
5062        if (sentMailbox == null) return null;
5063        final Uri messageUri = uiSaveMessage(msg, mailbox, extras);
5064        // Kick observers
5065        notifyUI(Mailbox.CONTENT_URI, null);
5066        return messageUri;
5067    }
5068
5069    private static void putIntegerLongOrBoolean(ContentValues values, String columnName,
5070            Object value) {
5071        if (value instanceof Integer) {
5072            Integer intValue = (Integer)value;
5073            values.put(columnName, intValue);
5074        } else if (value instanceof Boolean) {
5075            Boolean boolValue = (Boolean)value;
5076            values.put(columnName, boolValue ? 1 : 0);
5077        } else if (value instanceof Long) {
5078            Long longValue = (Long)value;
5079            values.put(columnName, longValue);
5080        }
5081    }
5082
5083    /**
5084     * Update the timestamps for the folders specified and notifies on the recent folder URI.
5085     * @param folders array of folder Uris to update
5086     * @return number of folders updated
5087     */
5088    private int updateTimestamp(final Context context, String id, Uri[] folders){
5089        int updated = 0;
5090        final long now = System.currentTimeMillis();
5091        final ContentResolver resolver = context.getContentResolver();
5092        final ContentValues touchValues = new ContentValues(1);
5093        for (final Uri folder : folders) {
5094            touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now);
5095            LogUtils.d(TAG, "updateStamp: %s updated", folder);
5096            updated += resolver.update(folder, touchValues, null, null);
5097        }
5098        final Uri toNotify =
5099                UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build();
5100        LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify);
5101        notifyUI(toNotify, null);
5102        return updated;
5103    }
5104
5105    /**
5106     * Updates the recent folders. The values to be updated are specified as ContentValues pairs
5107     * of (Folder URI, access timestamp). Returns nonzero if successful, always.
5108     * @param uri provider query uri
5109     * @param values uri, timestamp pairs
5110     * @return nonzero value always.
5111     */
5112    private int uiUpdateRecentFolders(Uri uri, ContentValues values) {
5113        final int numFolders = values.size();
5114        final String id = uri.getPathSegments().get(1);
5115        final Uri[] folders = new Uri[numFolders];
5116        final Context context = getContext();
5117        int i = 0;
5118        for (final String uriString : values.keySet()) {
5119            folders[i] = Uri.parse(uriString);
5120        }
5121        return updateTimestamp(context, id, folders);
5122    }
5123
5124    /**
5125     * Populates the recent folders according to the design.
5126     * @param uri provider query uri
5127     * @return the number of recent folders were populated.
5128     */
5129    private int uiPopulateRecentFolders(Uri uri) {
5130        final Context context = getContext();
5131        final String id = uri.getLastPathSegment();
5132        final Uri[] recentFolders = defaultRecentFolders(id);
5133        final int numFolders = recentFolders.length;
5134        if (numFolders <= 0) {
5135            return 0;
5136        }
5137        final int rowsUpdated = updateTimestamp(context, id, recentFolders);
5138        LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated);
5139        return rowsUpdated;
5140    }
5141
5142    private int uiUpdateAttachment(Uri uri, ContentValues uiValues) {
5143        int result = 0;
5144        Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE);
5145        if (stateValue != null) {
5146            // This is a command from UIProvider
5147            long attachmentId = Long.parseLong(uri.getLastPathSegment());
5148            Context context = getContext();
5149            Attachment attachment =
5150                    Attachment.restoreAttachmentWithId(context, attachmentId);
5151            if (attachment == null) {
5152                // Went away; ah, well...
5153                return result;
5154            }
5155            int state = stateValue;
5156            ContentValues values = new ContentValues();
5157            if (state == UIProvider.AttachmentState.NOT_SAVED
5158                    || state == UIProvider.AttachmentState.REDOWNLOADING) {
5159                // Set state, try to cancel request
5160                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED);
5161                values.put(AttachmentColumns.FLAGS,
5162                        attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST);
5163                attachment.update(context, values);
5164                result = 1;
5165            }
5166            if (state == UIProvider.AttachmentState.DOWNLOADING
5167                    || state == UIProvider.AttachmentState.REDOWNLOADING) {
5168                // Set state and destination; request download
5169                values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING);
5170                Integer destinationValue =
5171                        uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
5172                values.put(AttachmentColumns.UI_DESTINATION,
5173                        destinationValue == null ? 0 : destinationValue);
5174                values.put(AttachmentColumns.FLAGS,
5175                        attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
5176
5177                if (values.containsKey(AttachmentColumns.LOCATION) &&
5178                        TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
5179                    LogUtils.w(TAG, new Throwable(), "attachment with blank location");
5180                }
5181
5182                attachment.update(context, values);
5183                result = 1;
5184            }
5185            if (state == UIProvider.AttachmentState.SAVED) {
5186                // If this is an inline attachment, notify message has changed
5187                if (!TextUtils.isEmpty(attachment.mContentId)) {
5188                    notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey);
5189                }
5190                result = 1;
5191            }
5192        }
5193        return result;
5194    }
5195
5196    private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) {
5197        // We need to mark seen separately
5198        if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) {
5199            final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN);
5200
5201            if (seenValue == 1) {
5202                final String mailboxId = uri.getLastPathSegment();
5203                final int rows = markAllSeen(context, mailboxId);
5204
5205                if (uiValues.size() == 1) {
5206                    // Nothing else to do, so return this value
5207                    return rows;
5208                }
5209            }
5210        }
5211
5212        final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true);
5213        if (ourUri == null) return 0;
5214        ContentValues ourValues = new ContentValues();
5215        // This should only be called via update to "recent folders"
5216        for (String columnName: uiValues.keySet()) {
5217            if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) {
5218                ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName));
5219            }
5220        }
5221        return update(ourUri, ourValues, null, null);
5222    }
5223
5224    private int uiUpdateSettings(final Context c, final ContentValues uiValues) {
5225        final MailPrefs mailPrefs = MailPrefs.get(c);
5226
5227        if (uiValues.containsKey(SettingsColumns.AUTO_ADVANCE)) {
5228            mailPrefs.setAutoAdvanceMode(uiValues.getAsInteger(SettingsColumns.AUTO_ADVANCE));
5229        }
5230        if (uiValues.containsKey(SettingsColumns.CONVERSATION_VIEW_MODE)) {
5231            final int value = uiValues.getAsInteger(SettingsColumns.CONVERSATION_VIEW_MODE);
5232            final boolean overviewMode = value == UIProvider.ConversationViewMode.OVERVIEW;
5233            mailPrefs.setConversationOverviewMode(overviewMode);
5234        }
5235
5236        c.getContentResolver().notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null, false);
5237
5238        return 1;
5239    }
5240
5241    private int markAllSeen(final Context context, final String mailboxId) {
5242        final SQLiteDatabase db = getDatabase(context);
5243        final String table = Message.TABLE_NAME;
5244        final ContentValues values = new ContentValues(1);
5245        values.put(MessageColumns.FLAG_SEEN, 1);
5246        final String whereClause = MessageColumns.MAILBOX_KEY + " = ?";
5247        final String[] whereArgs = new String[] {mailboxId};
5248
5249        return db.update(table, values, whereClause, whereArgs);
5250    }
5251
5252    private ContentValues convertUiMessageValues(Message message, ContentValues values) {
5253        final ContentValues ourValues = new ContentValues();
5254        for (String columnName : values.keySet()) {
5255            final Object val = values.get(columnName);
5256            if (columnName.equals(UIProvider.ConversationColumns.STARRED)) {
5257                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val);
5258            } else if (columnName.equals(UIProvider.ConversationColumns.READ)) {
5259                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val);
5260            } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) {
5261                putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val);
5262            } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
5263                putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val);
5264            } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) {
5265                // Skip this column, as the folders will also be specified  the RAW_FOLDERS column
5266            } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) {
5267                // Convert from folder list uri to mailbox key
5268                final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName));
5269                if (flist.folders.size() != 1) {
5270                    LogUtils.e(TAG,
5271                            "Incorrect number of folders for this message: Message is %s",
5272                            message.mId);
5273                } else {
5274                    final Folder f = flist.folders.get(0);
5275                    final Uri uri = f.folderUri.fullUri;
5276                    final Long mailboxId = Long.parseLong(uri.getLastPathSegment());
5277                    putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId);
5278                }
5279            } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) {
5280                Address[] fromList = Address.fromHeader(message.mFrom);
5281                final MailPrefs mailPrefs = MailPrefs.get(getContext());
5282                for (Address sender : fromList) {
5283                    final String email = sender.getAddress();
5284                    mailPrefs.setDisplayImagesFromSender(email, null);
5285                }
5286            } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) ||
5287                    columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) {
5288                // Ignore for now
5289            } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) {
5290                // Email's conversation info is generated, not stored, so just ignore this update
5291            } else {
5292                throw new IllegalArgumentException("Can't update " + columnName + " in message");
5293            }
5294        }
5295        return ourValues;
5296    }
5297
5298    private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) {
5299        final String idString = uri.getLastPathSegment();
5300        try {
5301            final long id = Long.parseLong(idString);
5302            Uri ourUri = ContentUris.withAppendedId(newBaseUri, id);
5303            if (asProvider) {
5304                ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build();
5305            }
5306            return ourUri;
5307        } catch (NumberFormatException e) {
5308            return null;
5309        }
5310    }
5311
5312    private Message getMessageFromLastSegment(Uri uri) {
5313        long messageId = Long.parseLong(uri.getLastPathSegment());
5314        return Message.restoreMessageWithId(getContext(), messageId);
5315    }
5316
5317    /**
5318     * Add an undo operation for the current sequence; if the sequence is newer than what we've had,
5319     * clear out the undo list and start over
5320     * @param uri the uri we're working on
5321     * @param op the ContentProviderOperation to perform upon undo
5322     */
5323    private void addToSequence(Uri uri, ContentProviderOperation op) {
5324        String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER);
5325        if (sequenceString != null) {
5326            int sequence = Integer.parseInt(sequenceString);
5327            if (sequence > mLastSequence) {
5328                // Reset sequence
5329                mLastSequenceOps.clear();
5330                mLastSequence = sequence;
5331            }
5332            // TODO: Need something to indicate a change isn't ready (undoable)
5333            mLastSequenceOps.add(op);
5334        }
5335    }
5336
5337    // TODO: This should depend on flags on the mailbox...
5338    private static boolean uploadsToServer(Context context, Mailbox m) {
5339        if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
5340                m.mType == Mailbox.TYPE_SEARCH) {
5341            return false;
5342        }
5343        String protocol = Account.getProtocol(context, m.mAccountKey);
5344        EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
5345        return (info != null && info.syncChanges);
5346    }
5347
5348    private int uiUpdateMessage(Uri uri, ContentValues values) {
5349        return uiUpdateMessage(uri, values, false);
5350    }
5351
5352    private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) {
5353        Context context = getContext();
5354        Message msg = getMessageFromLastSegment(uri);
5355        if (msg == null) return 0;
5356        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
5357        if (mailbox == null) return 0;
5358        Uri ourBaseUri =
5359                (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI :
5360                    Message.CONTENT_URI;
5361        Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true);
5362        if (ourUri == null) return 0;
5363
5364        // Special case - meeting response
5365        if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) {
5366            final EmailServiceProxy service =
5367                    EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey);
5368            try {
5369                service.sendMeetingResponse(msg.mId,
5370                        values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN));
5371                // Delete the message immediately
5372                uiDeleteMessage(uri);
5373                Utility.showToast(context, R.string.confirm_response);
5374                // Notify box has changed so the deletion is reflected in the UI
5375                notifyUIConversationMailbox(mailbox.mId);
5376            } catch (RemoteException e) {
5377                LogUtils.d(TAG, "Remote exception while sending meeting response");
5378            }
5379            return 1;
5380        }
5381
5382        // Another special case - deleting a draft.
5383        final String operation = values.getAsString(
5384                UIProvider.ConversationOperations.OPERATION_KEY);
5385        // TODO: for now let's just default to delete for MOVE_FAILED_TO_DRAFT operation
5386        if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation) ||
5387                UIProvider.ConversationOperations.MOVE_FAILED_TO_DRAFTS.equals(operation)) {
5388            uiDeleteMessage(uri);
5389            return 1;
5390        }
5391
5392        ContentValues undoValues = new ContentValues();
5393        ContentValues ourValues = convertUiMessageValues(msg, values);
5394        for (String columnName: ourValues.keySet()) {
5395            if (columnName.equals(MessageColumns.MAILBOX_KEY)) {
5396                undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey);
5397            } else if (columnName.equals(MessageColumns.FLAG_READ)) {
5398                undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead);
5399            } else if (columnName.equals(MessageColumns.FLAG_SEEN)) {
5400                undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen);
5401            } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) {
5402                undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite);
5403            }
5404        }
5405        if (undoValues.size() == 0) {
5406            return -1;
5407        }
5408        final Boolean suppressUndo =
5409                values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO);
5410        if (suppressUndo == null || !suppressUndo) {
5411            final ContentProviderOperation op =
5412                    ContentProviderOperation.newUpdate(convertToEmailProviderUri(
5413                            uri, ourBaseUri, false))
5414                            .withValues(undoValues)
5415                            .build();
5416            addToSequence(uri, op);
5417        }
5418
5419        return update(ourUri, ourValues, null, null);
5420    }
5421
5422    /**
5423     * Projection for use with getting mailbox & account keys for a message.
5424     */
5425    private static final String[] MESSAGE_KEYS_PROJECTION =
5426            { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY };
5427    private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0;
5428    private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1;
5429
5430    /**
5431     * Notify necessary UI components in response to a message update.
5432     * @param uri The {@link Uri} for this message update.
5433     * @param messageId The id of the message that's been updated.
5434     * @param values The {@link ContentValues} that were updated in the message.
5435     */
5436    private void handleMessageUpdateNotifications(final Uri uri, final String messageId,
5437            final ContentValues values) {
5438        if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) {
5439            notifyUIConversation(uri);
5440        }
5441        notifyUIMessage(messageId);
5442        // TODO: Ideally, also test that the values actually changed.
5443        if (values.containsKey(MessageColumns.FLAG_READ) ||
5444                values.containsKey(MessageColumns.MAILBOX_KEY)) {
5445            final Cursor c = query(
5446                    Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(),
5447                    MESSAGE_KEYS_PROJECTION, null, null, null);
5448            if (c != null) {
5449                try {
5450                    if (c.moveToFirst()) {
5451                        notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN),
5452                                c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN));
5453                    }
5454                } finally {
5455                    c.close();
5456                }
5457            }
5458        }
5459    }
5460
5461    /**
5462     * Perform a "Delete" operation
5463     * @param uri message to delete
5464     * @return number of rows affected
5465     */
5466    private int uiDeleteMessage(Uri uri) {
5467        final Context context = getContext();
5468        Message msg = getMessageFromLastSegment(uri);
5469        if (msg == null) return 0;
5470        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey);
5471        if (mailbox == null) return 0;
5472        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) {
5473            // We actually delete these, including attachments
5474            AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId);
5475            final int r = context.getContentResolver().delete(
5476                    ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null);
5477            notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
5478            notifyUIMessage(msg.mId);
5479            return r;
5480        }
5481        Mailbox trashMailbox =
5482                Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH);
5483        if (trashMailbox == null) {
5484            return 0;
5485        }
5486        ContentValues values = new ContentValues();
5487        values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId);
5488        final int r = uiUpdateMessage(uri, values, true);
5489        notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
5490        notifyUIMessage(msg.mId);
5491        return r;
5492    }
5493
5494    /**
5495     * Hard delete all synced messages in a particular mailbox
5496     * @param uri Mailbox to empty (Trash, or maybe Spam/Junk later)
5497     * @return number of rows affected
5498     */
5499    private int uiPurgeFolder(Uri uri) {
5500        final Context context = getContext();
5501        final long mailboxId = Long.parseLong(uri.getLastPathSegment());
5502        final SQLiteDatabase db = getDatabase(context);
5503
5504        // Find the account ID (needed in a few calls)
5505        final Cursor mailboxCursor = db.query(
5506                Mailbox.TABLE_NAME, new String[] { MailboxColumns.ACCOUNT_KEY },
5507                Mailbox._ID + "=" + mailboxId, null, null, null, null);
5508        if (mailboxCursor == null || !mailboxCursor.moveToFirst()) {
5509            LogUtils.wtf(LogUtils.TAG, "Null or empty cursor when trying to purge mailbox %d",
5510                    mailboxId);
5511            return 0;
5512        }
5513        final long accountId = mailboxCursor.getLong(mailboxCursor.getColumnIndex(
5514                MailboxColumns.ACCOUNT_KEY));
5515
5516        // Find all the messages in the mailbox
5517        final String[] messageProjection =
5518                new String[] { MessageColumns._ID };
5519        final String messageWhere = MessageColumns.MAILBOX_KEY + "=" + mailboxId;
5520        final Cursor messageCursor = db.query(Message.TABLE_NAME, messageProjection, messageWhere,
5521                null, null, null, null);
5522        int deletedCount = 0;
5523
5524        // Kill them with fire
5525        while (messageCursor != null && messageCursor.moveToNext()) {
5526            final long messageId = messageCursor.getLong(messageCursor.getColumnIndex(
5527                    MessageColumns._ID));
5528            AttachmentUtilities.deleteAllAttachmentFiles(context, accountId, messageId);
5529            deletedCount += context.getContentResolver().delete(
5530                    ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, messageId), null, null);
5531            notifyUIMessage(messageId);
5532        }
5533
5534        notifyUIFolder(mailboxId, accountId);
5535        return deletedCount;
5536    }
5537
5538    public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
5539    public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
5540    // Currently unused
5541    //public static final String PICKER_MESSAGE_ID = "picker_message_id";
5542    public static final String PICKER_HEADER_ID = "picker_header_id";
5543
5544    private int pickFolder(Uri uri, int type, int headerId) {
5545        Context context = getContext();
5546        Long acctId = Long.parseLong(uri.getLastPathSegment());
5547        // For push imap, for example, we want the user to select the trash mailbox
5548        Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION,
5549                null, null, null);
5550        try {
5551            if (ac.moveToFirst()) {
5552                final com.android.mail.providers.Account uiAccount =
5553                        com.android.mail.providers.Account.builder().buildFrom(ac);
5554                Intent intent = new Intent(context, FolderPickerActivity.class);
5555                intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
5556                intent.putExtra(PICKER_MAILBOX_TYPE, type);
5557                intent.putExtra(PICKER_HEADER_ID, headerId);
5558                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5559                context.startActivity(intent);
5560                return 1;
5561            }
5562            return 0;
5563        } finally {
5564            ac.close();
5565        }
5566    }
5567
5568    private int pickTrashFolder(Uri uri) {
5569        return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
5570    }
5571
5572    private int pickSentFolder(Uri uri) {
5573        return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
5574    }
5575
5576    private Cursor uiUndo(String[] projection) {
5577        // First see if we have any operations saved
5578        // TODO: Make sure seq matches
5579        if (!mLastSequenceOps.isEmpty()) {
5580            try {
5581                // TODO Always use this projection?  Or what's passed in?
5582                // Not sure if UI wants it, but I'm making a cursor of convo uri's
5583                MatrixCursor c = new MatrixCursorWithCachedColumns(
5584                        new String[] {UIProvider.ConversationColumns.URI},
5585                        mLastSequenceOps.size());
5586                for (ContentProviderOperation op: mLastSequenceOps) {
5587                    c.addRow(new String[] {op.getUri().toString()});
5588                }
5589                // Just apply the batch and we're done!
5590                applyBatch(mLastSequenceOps);
5591                // But clear the operations
5592                mLastSequenceOps.clear();
5593                return c;
5594            } catch (OperationApplicationException e) {
5595                LogUtils.d(TAG, "applyBatch exception");
5596            }
5597        }
5598        return new MatrixCursorWithCachedColumns(projection, 0);
5599    }
5600
5601    private void notifyUIConversation(Uri uri) {
5602        String id = uri.getLastPathSegment();
5603        Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id));
5604        if (msg != null) {
5605            notifyUIConversationMailbox(msg.mMailboxKey);
5606        }
5607    }
5608
5609    /**
5610     * Notify about the Mailbox id passed in
5611     * @param id the Mailbox id to be notified
5612     */
5613    private void notifyUIConversationMailbox(long id) {
5614        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id));
5615        Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id);
5616        if (mailbox == null) {
5617            LogUtils.w(TAG, "No mailbox for notification: " + id);
5618            return;
5619        }
5620        // Notify combined inbox...
5621        if (mailbox.mType == Mailbox.TYPE_INBOX) {
5622            notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER,
5623                    EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX));
5624        }
5625        notifyWidgets(id);
5626    }
5627
5628    /**
5629     * Notify about the message id passed in
5630     * @param id the message id to be notified
5631     */
5632    private void notifyUIMessage(long id) {
5633        notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
5634    }
5635
5636    /**
5637     * Notify about the message id passed in
5638     * @param id the message id to be notified
5639     */
5640    private void notifyUIMessage(String id) {
5641        notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id);
5642    }
5643
5644    /**
5645     * Notify about the Account id passed in
5646     * @param id the Account id to be notified
5647     */
5648    private void notifyUIAccount(long id) {
5649        // Notify on the specific account
5650        notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id));
5651
5652        // Notify on the all accounts list
5653        notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
5654    }
5655
5656    // TODO: temporary workaround for ConversationCursor
5657    @Deprecated
5658    private static final int NOTIFY_FOLDER_LOOP_MESSAGE_ID = 0;
5659    @Deprecated
5660    private Handler mFolderNotifierHandler;
5661
5662    /**
5663     * Notify about a folder update. Because folder changes can affect the conversation cursor's
5664     * extras, the conversation must also be notified here.
5665     * @param folderId the folder id to be notified
5666     * @param accountId the account id to be notified (for folder list notification).
5667     */
5668    private void notifyUIFolder(final String folderId, final long accountId) {
5669        notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
5670        notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId);
5671        if (accountId != Account.NO_ACCOUNT) {
5672            notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId);
5673        }
5674
5675        // Notify for combined account too
5676        // TODO: might be nice to only notify when an inbox changes
5677        notifyUI(UIPROVIDER_FOLDER_NOTIFIER,
5678                getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX));
5679        notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID);
5680
5681        // TODO: temporary workaround for ConversationCursor
5682        synchronized (this) {
5683            if (mFolderNotifierHandler == null) {
5684                mFolderNotifierHandler = new Handler(Looper.getMainLooper(),
5685                        new Callback() {
5686                            @Override
5687                            public boolean handleMessage(final android.os.Message message) {
5688                                final String folderId = (String) message.obj;
5689                                LogUtils.d(TAG, "Notifying conversation Uri %s twice", folderId);
5690                                notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId);
5691                                return true;
5692                            }
5693                        });
5694            }
5695        }
5696        mFolderNotifierHandler.removeMessages(NOTIFY_FOLDER_LOOP_MESSAGE_ID);
5697        android.os.Message message = android.os.Message.obtain(mFolderNotifierHandler,
5698                NOTIFY_FOLDER_LOOP_MESSAGE_ID);
5699        message.obj = folderId;
5700        mFolderNotifierHandler.sendMessageDelayed(message, 2000);
5701    }
5702
5703    private void notifyUIFolder(final long folderId, final long accountId) {
5704        notifyUIFolder(Long.toString(folderId), accountId);
5705    }
5706
5707    private void notifyUI(final Uri uri, final String id) {
5708        final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri;
5709        final Set<Uri> batchNotifications = getBatchNotificationsSet();
5710        if (batchNotifications != null) {
5711            batchNotifications.add(notifyUri);
5712        } else {
5713            getContext().getContentResolver().notifyChange(notifyUri, null);
5714        }
5715    }
5716
5717    private void notifyUI(Uri uri, long id) {
5718        notifyUI(uri, Long.toString(id));
5719    }
5720
5721    private Mailbox getMailbox(final Uri uri) {
5722        final long id = Long.parseLong(uri.getLastPathSegment());
5723        return Mailbox.restoreMailboxWithId(getContext(), id);
5724    }
5725
5726    /**
5727     * Create an android.accounts.Account object for this account.
5728     * @param accountId id of account to load.
5729     * @return an android.accounts.Account for this account, or null if we can't load it.
5730     */
5731    private android.accounts.Account getAccountManagerAccount(final long accountId) {
5732        final Context context = getContext();
5733        final Account account = Account.restoreAccountWithId(context, accountId);
5734        if (account == null) return null;
5735        return getAccountManagerAccount(context, account.mEmailAddress,
5736                account.getProtocol(context));
5737    }
5738
5739    /**
5740     * Create an android.accounts.Account object for an emailAddress/protocol pair.
5741     * @param context A {@link Context}.
5742     * @param emailAddress The email address we're interested in.
5743     * @param protocol The protocol we're intereted in.
5744     * @return an {@link android.accounts.Account} for this info.
5745     */
5746    private static android.accounts.Account getAccountManagerAccount(final Context context,
5747            final String emailAddress, final String protocol) {
5748        final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
5749        if (info == null) {
5750            return null;
5751        }
5752        return new android.accounts.Account(emailAddress, info.accountType);
5753    }
5754
5755    /**
5756     * Update an account's periodic sync if the sync interval has changed.
5757     * @param accountId id for the account to update.
5758     * @param values the ContentValues for this update to the account.
5759     */
5760    private void updateAccountSyncInterval(final long accountId, final ContentValues values) {
5761        final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL);
5762        if (syncInterval == null) {
5763            // No change to the sync interval.
5764            return;
5765        }
5766        final android.accounts.Account account = getAccountManagerAccount(accountId);
5767        if (account == null) {
5768            // Unable to load the account, or unknown protocol.
5769            return;
5770        }
5771
5772        LogUtils.d(TAG, "Setting sync interval for account %s to %d minutes",
5773                accountId, syncInterval);
5774
5775        // First remove all existing periodic syncs.
5776        final List<PeriodicSync> syncs =
5777                ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY);
5778        for (final PeriodicSync sync : syncs) {
5779            ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras);
5780        }
5781
5782        // Only positive values of sync interval indicate periodic syncs. The value is in minutes,
5783        // while addPeriodicSync expects its time in seconds.
5784        if (syncInterval > 0) {
5785            ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY,
5786                    syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
5787        }
5788    }
5789
5790    /**
5791     * Request a sync.
5792     * @param account The {@link android.accounts.Account} we want to sync.
5793     * @param mailboxId The mailbox id we want to sync (or one of the special constants in
5794     *                  {@link Mailbox}).
5795     * @param deltaMessageCount If we're requesting a load more, the number of additional messages
5796     *                          to sync.
5797     */
5798    private static void startSync(final android.accounts.Account account, final long mailboxId,
5799            final int deltaMessageCount) {
5800        final Bundle extras = Mailbox.createSyncBundle(mailboxId);
5801        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
5802        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
5803        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
5804        if (deltaMessageCount != 0) {
5805            extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount);
5806        }
5807        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
5808                EmailContent.CONTENT_URI.toString());
5809        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
5810                SYNC_STATUS_CALLBACK_METHOD);
5811        ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
5812        LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
5813                extras.toString());
5814    }
5815
5816    /**
5817     * Request a sync.
5818     * @param mailbox The {@link Mailbox} we want to sync.
5819     * @param deltaMessageCount If we're requesting a load more, the number of additional messages
5820     *                          to sync.
5821     */
5822    private void startSync(final Mailbox mailbox, final int deltaMessageCount) {
5823        final android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey);
5824        if (account != null) {
5825            startSync(account, mailbox.mId, deltaMessageCount);
5826        }
5827    }
5828
5829    /**
5830     * Restart any push operations for an account.
5831     * @param account The {@link android.accounts.Account} we're interested in.
5832     */
5833    private static void restartPush(final android.accounts.Account account) {
5834        final Bundle extras = new Bundle();
5835        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
5836        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
5837        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
5838        extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
5839        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI,
5840                EmailContent.CONTENT_URI.toString());
5841        extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD,
5842                SYNC_STATUS_CALLBACK_METHOD);
5843        ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras);
5844        LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(),
5845                extras.toString());
5846    }
5847
5848    private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) {
5849        if (mailbox != null) {
5850            RefreshStatusMonitor.getInstance(getContext())
5851                    .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() {
5852                @Override
5853                public void onRefreshCompleted(long mailboxId, int result) {
5854                    // all calls to this method assumed to be started by a user action
5855                    final int syncValue = UIProvider.createSyncValue(EmailContent.SYNC_STATUS_USER,
5856                            result);
5857                    final ContentValues values = new ContentValues();
5858                    values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
5859                    values.put(Mailbox.UI_LAST_SYNC_RESULT, syncValue);
5860                    mDatabase.update(Mailbox.TABLE_NAME, values, WHERE_ID,
5861                            new String[] { String.valueOf(mailboxId) });
5862                    notifyUIFolder(mailbox.mId, mailbox.mAccountKey);
5863                }
5864
5865                @Override
5866                public void onTimeout(long mailboxId) {
5867                    // todo
5868                }
5869            });
5870            startSync(mailbox, deltaMessageCount);
5871        }
5872        return null;
5873    }
5874
5875    //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes
5876    public static final int VISIBLE_LIMIT_INCREMENT = 10;
5877    //Number of additional messages to load when a user selects "Load more..." in a search
5878    public static final int SEARCH_MORE_INCREMENT = 10;
5879
5880    private Cursor uiFolderLoadMore(final Mailbox mailbox) {
5881        if (mailbox == null) return null;
5882        if (mailbox.mType == Mailbox.TYPE_SEARCH) {
5883            // Ask for 10 more messages
5884            mSearchParams.mOffset += SEARCH_MORE_INCREMENT;
5885            runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId);
5886        } else {
5887            uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT);
5888        }
5889        return null;
5890    }
5891
5892    private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
5893    private SearchParams mSearchParams;
5894
5895    /**
5896     * Returns the search mailbox for the specified account, creating one if necessary
5897     * @return the search mailbox for the passed in account
5898     */
5899    private Mailbox getSearchMailbox(long accountId) {
5900        Context context = getContext();
5901        Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH);
5902        if (m == null) {
5903            m = new Mailbox();
5904            m.mAccountKey = accountId;
5905            m.mServerId = SEARCH_MAILBOX_SERVER_ID;
5906            m.mFlagVisible = false;
5907            m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
5908            m.mSyncInterval = 0;
5909            m.mType = Mailbox.TYPE_SEARCH;
5910            m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
5911            m.mParentKey = Mailbox.NO_MAILBOX;
5912            m.save(context);
5913        }
5914        return m;
5915    }
5916
5917    private void runSearchQuery(final Context context, final long accountId,
5918            final long searchMailboxId) {
5919        LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d",
5920                accountId, searchMailboxId);
5921
5922        // Start the search running in the background
5923        new AsyncTask<Void, Void, Void>() {
5924            @Override
5925            public Void doInBackground(Void... params) {
5926                final EmailServiceProxy service =
5927                        EmailServiceUtils.getServiceForAccount(context, accountId);
5928                if (service != null) {
5929                    try {
5930                        final int totalCount =
5931                                service.searchMessages(accountId, mSearchParams, searchMailboxId);
5932
5933                        // Save away the total count
5934                        final ContentValues cv = new ContentValues(1);
5935                        cv.put(MailboxColumns.TOTAL_COUNT, totalCount);
5936                        update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), cv,
5937                                null, null);
5938                        LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d",
5939                                totalCount);
5940                    } catch (RemoteException e) {
5941                        LogUtils.e("searchMessages", "RemoteException", e);
5942                    }
5943                }
5944                return null;
5945            }
5946        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
5947    }
5948
5949    // This handles an initial search query. More results are loaded using uiFolderLoadMore.
5950    private Cursor uiSearch(Uri uri, String[] projection) {
5951        LogUtils.d(TAG, "runSearchQuery in search %s", uri);
5952        final long accountId = Long.parseLong(uri.getLastPathSegment());
5953
5954        // TODO: Check the actual mailbox
5955        Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX);
5956        if (inbox == null) {
5957            LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account "
5958                    + accountId);
5959
5960            return null;
5961        }
5962
5963        String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY);
5964        if (filter == null) {
5965            throw new IllegalArgumentException("No query parameter in search query");
5966        }
5967
5968        // Find/create our search mailbox
5969        Mailbox searchMailbox = getSearchMailbox(accountId);
5970        final long searchMailboxId = searchMailbox.mId;
5971
5972        mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId);
5973
5974        final Context context = getContext();
5975        if (mSearchParams.mOffset == 0) {
5976            // TODO: This conditional is unnecessary, just two lines earlier we created
5977            // mSearchParams using a constructor that never sets mOffset.
5978            LogUtils.d(TAG, "deleting existing search results.");
5979            final ContentResolver resolver = context.getContentResolver();
5980            final ContentValues cv = new ContentValues(3);
5981            // For now, use the actual query as the name of the mailbox
5982            cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter);
5983            // We are about to do a sync on this folder, but if the UI is refreshed before the
5984            // service can start its query, we need it to see that there is a sync in progress.
5985            // Otherwise it could show the empty state, until the service gets around to setting
5986            // the syncState.
5987            cv.put(Mailbox.UI_SYNC_STATUS, EmailContent.SYNC_STATUS_LIVE);
5988            // We don't know how many result we'll have yet, but we assume zero until we get
5989            // a response back from the server. Otherwise, we'll whatever count there was on the
5990            // previous search, and we'll display the "Load More" footer prior to having
5991            // any results.
5992            cv.put(Mailbox.TOTAL_COUNT, 0);
5993            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
5994                    cv, null, null);
5995
5996            // Delete existing contents of search mailbox
5997            resolver.delete(Message.CONTENT_URI, MessageColumns.MAILBOX_KEY + "=" + searchMailboxId,
5998                    null);
5999        }
6000
6001        // Start the search running in the background
6002        runSearchQuery(context, accountId, searchMailboxId);
6003
6004        // This will look just like a "normal" folder
6005        return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI,
6006                searchMailbox.mId), projection, false);
6007    }
6008
6009    private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
6010
6011    /**
6012     * Delete an account and clean it up
6013     */
6014    private int uiDeleteAccount(Uri uri) {
6015        Context context = getContext();
6016        long accountId = Long.parseLong(uri.getLastPathSegment());
6017        try {
6018            // Get the account URI.
6019            final Account account = Account.restoreAccountWithId(context, accountId);
6020            if (account == null) {
6021                return 0; // Already deleted?
6022            }
6023
6024            deleteAccountData(context, accountId);
6025
6026            // Now delete the account itself
6027            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
6028            context.getContentResolver().delete(uri, null, null);
6029
6030            // Clean up
6031            AccountBackupRestore.backup(context);
6032            SecurityPolicy.getInstance(context).reducePolicies();
6033            setServicesEnabledSync(context);
6034            // TODO: We ought to reconcile accounts here, but some callers do this in a loop,
6035            // which would be a problem when the first account reconciliation shuts us down.
6036            return 1;
6037        } catch (Exception e) {
6038            LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e);
6039        }
6040        return 0;
6041    }
6042
6043    private int uiDeleteAccountData(Uri uri) {
6044        Context context = getContext();
6045        long accountId = Long.parseLong(uri.getLastPathSegment());
6046        // Get the account URI.
6047        final Account account = Account.restoreAccountWithId(context, accountId);
6048        if (account == null) {
6049            return 0; // Already deleted?
6050        }
6051        deleteAccountData(context, accountId);
6052        return 1;
6053    }
6054
6055    /**
6056     * The method will no longer be needed after platform L releases. As emails are received from
6057     * various protocols the email addresses are decoded and intended to be stored in the database
6058     * in decoded form. The problem is that Exchange is a separate .apk and the old Exchange .apk
6059     * still attempts to store <strong>encoded</strong> email addresses. So, we decode here at the
6060     * Provider before writing to the database to ensure the addresses are written in decoded form.
6061     *
6062     * @param values the values to be written into the Message table
6063     */
6064    private static void decodeEmailAddresses(ContentValues values) {
6065        if (values.containsKey(Message.MessageColumns.TO_LIST)) {
6066            final String to = values.getAsString(Message.MessageColumns.TO_LIST);
6067            values.put(Message.MessageColumns.TO_LIST, Address.fromHeaderToString(to));
6068        }
6069
6070        if (values.containsKey(Message.MessageColumns.FROM_LIST)) {
6071            final String from = values.getAsString(Message.MessageColumns.FROM_LIST);
6072            values.put(Message.MessageColumns.FROM_LIST, Address.fromHeaderToString(from));
6073        }
6074
6075        if (values.containsKey(Message.MessageColumns.CC_LIST)) {
6076            final String cc = values.getAsString(Message.MessageColumns.CC_LIST);
6077            values.put(Message.MessageColumns.CC_LIST, Address.fromHeaderToString(cc));
6078        }
6079
6080        if (values.containsKey(Message.MessageColumns.BCC_LIST)) {
6081            final String bcc = values.getAsString(Message.MessageColumns.BCC_LIST);
6082            values.put(Message.MessageColumns.BCC_LIST, Address.fromHeaderToString(bcc));
6083        }
6084
6085        if (values.containsKey(Message.MessageColumns.REPLY_TO_LIST)) {
6086            final String replyTo = values.getAsString(Message.MessageColumns.REPLY_TO_LIST);
6087            values.put(Message.MessageColumns.REPLY_TO_LIST,
6088                    Address.fromHeaderToString(replyTo));
6089        }
6090    }
6091
6092    /** Projection used for getting email address for an account. */
6093    private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
6094
6095    private static void deleteAccountData(Context context, long accountId) {
6096        // We will delete PIM data, but by the time the asynchronous call to do that happens,
6097        // the account may have been deleted from the DB. Therefore we have to get the email
6098        // address now and send that, rather than the account id.
6099        final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI,
6100                ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
6101                new String[] {Long.toString(accountId)}, null, 0);
6102        if (emailAddress == null) {
6103            LogUtils.e(TAG, "Could not find email address for account %d", accountId);
6104        }
6105
6106        // Delete synced attachments
6107        AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId);
6108
6109        // Delete all mailboxes.
6110        ContentResolver resolver = context.getContentResolver();
6111        String[] accountIdArgs = new String[] { Long.toString(accountId) };
6112        resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
6113
6114        // Delete account sync key.
6115        final ContentValues cv = new ContentValues();
6116        cv.putNull(AccountColumns.SYNC_KEY);
6117        resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
6118
6119        // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
6120        if (emailAddress != null) {
6121            final IEmailService service =
6122                    EmailServiceUtils.getServiceForAccount(context, accountId);
6123            if (service != null) {
6124                try {
6125                    service.deleteExternalAccountPIMData(emailAddress);
6126                } catch (final RemoteException e) {
6127                    // Can't do anything about this
6128                }
6129            }
6130        }
6131    }
6132
6133    private int[] mSavedWidgetIds = new int[0];
6134    private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>();
6135    private AppWidgetManager mAppWidgetManager;
6136    private ComponentName mEmailComponent;
6137
6138    private void notifyWidgets(long mailboxId) {
6139        Context context = getContext();
6140        // Lazily initialize these
6141        if (mAppWidgetManager == null) {
6142            mAppWidgetManager = AppWidgetManager.getInstance(context);
6143            mEmailComponent = new ComponentName(context, WidgetProvider.getProviderName(context));
6144        }
6145
6146        // See if we have to populate our array of mailboxes used in widgets
6147        int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent);
6148        if (!Arrays.equals(widgetIds, mSavedWidgetIds)) {
6149            mSavedWidgetIds = widgetIds;
6150            String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds);
6151            // widgetInfo now has pairs of account uri/folder uri
6152            mWidgetNotifyMailboxes.clear();
6153            for (String[] widgetInfo: widgetInfos) {
6154                try {
6155                    if (widgetInfo == null || TextUtils.isEmpty(widgetInfo[1])) continue;
6156                    long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment());
6157                    if (!isCombinedMailbox(id)) {
6158                        // For a regular mailbox, just add it to the list
6159                        if (!mWidgetNotifyMailboxes.contains(id)) {
6160                            mWidgetNotifyMailboxes.add(id);
6161                        }
6162                    } else {
6163                        switch (getVirtualMailboxType(id)) {
6164                            // We only handle the combined inbox in widgets
6165                            case Mailbox.TYPE_INBOX:
6166                                Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
6167                                        MailboxColumns.TYPE + "=?",
6168                                        new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null);
6169                                try {
6170                                    while (c.moveToNext()) {
6171                                        mWidgetNotifyMailboxes.add(
6172                                                c.getLong(Mailbox.ID_PROJECTION_COLUMN));
6173                                    }
6174                                } finally {
6175                                    c.close();
6176                                }
6177                                break;
6178                        }
6179                    }
6180                } catch (NumberFormatException e) {
6181                    // Move along
6182                }
6183            }
6184        }
6185
6186        // If our mailbox needs to be notified, do so...
6187        if (mWidgetNotifyMailboxes.contains(mailboxId)) {
6188            Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED);
6189            intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId));
6190            intent.setType(EMAIL_APP_MIME_TYPE);
6191            context.sendBroadcast(intent);
6192         }
6193    }
6194
6195    @Override
6196    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
6197        Context context = getContext();
6198        writer.println("Installed services:");
6199        for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) {
6200            writer.println("  " + info);
6201        }
6202        writer.println();
6203        writer.println("Accounts: ");
6204        Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
6205        if (cursor.getCount() == 0) {
6206            writer.println("  None");
6207        }
6208        try {
6209            while (cursor.moveToNext()) {
6210                Account account = new Account();
6211                account.restore(cursor);
6212                writer.println("  Account " + account.mDisplayName);
6213                HostAuth hostAuth =
6214                        HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
6215                if (hostAuth != null) {
6216                    writer.println("    Protocol = " + hostAuth.mProtocol +
6217                            (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " +
6218                                    account.mProtocolVersion));
6219                }
6220            }
6221        } finally {
6222            cursor.close();
6223        }
6224    }
6225
6226    synchronized public Handler getDelayedSyncHandler() {
6227        if (mDelayedSyncHandler == null) {
6228            mDelayedSyncHandler = new Handler(getContext().getMainLooper(), new Callback() {
6229                @Override
6230                public boolean handleMessage(android.os.Message msg) {
6231                    synchronized (mDelayedSyncRequests) {
6232                        final SyncRequestMessage request = (SyncRequestMessage) msg.obj;
6233                        // TODO: It's possible that the account is deleted by the time we get here
6234                        // It would be nice if we could validate it before trying to sync
6235                        final android.accounts.Account account = request.mAccount;
6236                        final Bundle extras = Mailbox.createSyncBundle(request.mMailboxId);
6237                        ContentResolver.requestSync(account, request.mAuthority, extras);
6238                        LogUtils.i(TAG, "requestSync getDelayedSyncHandler %s, %s",
6239                                account.toString(), extras.toString());
6240                        mDelayedSyncRequests.remove(request);
6241                        return true;
6242                    }
6243                }
6244            });
6245        }
6246        return mDelayedSyncHandler;
6247    }
6248
6249    private class SyncRequestMessage {
6250        private final String mAuthority;
6251        private final android.accounts.Account mAccount;
6252        private final long mMailboxId;
6253
6254        private SyncRequestMessage(final String authority, final android.accounts.Account account,
6255                final long mailboxId) {
6256            mAuthority = authority;
6257            mAccount = account;
6258            mMailboxId = mailboxId;
6259        }
6260
6261        @Override
6262        public boolean equals(Object o) {
6263            if (this == o) {
6264                return true;
6265            }
6266            if (o == null || getClass() != o.getClass()) {
6267                return false;
6268            }
6269
6270            SyncRequestMessage that = (SyncRequestMessage) o;
6271
6272            return mAccount.equals(that.mAccount)
6273                    && mMailboxId == that.mMailboxId
6274                    && mAuthority.equals(that.mAuthority);
6275        }
6276
6277        @Override
6278        public int hashCode() {
6279            int result = mAuthority.hashCode();
6280            result = 31 * result + mAccount.hashCode();
6281            result = 31 * result + (int) (mMailboxId ^ (mMailboxId >>> 32));
6282            return result;
6283        }
6284    }
6285
6286    @Override
6287    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
6288        if (PreferenceKeys.REMOVAL_ACTION.equals(key) ||
6289                PreferenceKeys.CONVERSATION_LIST_SWIPE.equals(key) ||
6290                PreferenceKeys.SHOW_SENDER_IMAGES.equals(key) ||
6291                PreferenceKeys.DEFAULT_REPLY_ALL.equals(key) ||
6292                PreferenceKeys.CONVERSATION_OVERVIEW_MODE.equals(key) ||
6293                PreferenceKeys.AUTO_ADVANCE_MODE.equals(key) ||
6294                PreferenceKeys.SNAP_HEADER_MODE.equals(key) ||
6295                PreferenceKeys.CONFIRM_DELETE.equals(key) ||
6296                PreferenceKeys.CONFIRM_ARCHIVE.equals(key) ||
6297                PreferenceKeys.CONFIRM_SEND.equals(key)) {
6298            notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null);
6299        }
6300    }
6301
6302    /**
6303     * Asynchronous version of {@link #setServicesEnabledSync(Context)}.  Use when calling from
6304     * UI thread (or lifecycle entry points.)
6305     */
6306    public static void setServicesEnabledAsync(final Context context) {
6307        if (context.getResources().getBoolean(R.bool.enable_services)) {
6308            EmailAsyncTask.runAsyncParallel(new Runnable() {
6309                @Override
6310                public void run() {
6311                    setServicesEnabledSync(context);
6312                }
6313            });
6314        }
6315    }
6316
6317    /**
6318     * Called throughout the application when the number of accounts has changed. This method
6319     * enables or disables the Compose activity, the boot receiver and the service based on
6320     * whether any accounts are configured.
6321     *
6322     * Blocking call - do not call from UI/lifecycle threads.
6323     *
6324     * @return true if there are any accounts configured.
6325     */
6326    public static boolean setServicesEnabledSync(Context context) {
6327        // Make sure we're initialized
6328        EmailContent.init(context);
6329        Cursor c = null;
6330        try {
6331            c = context.getContentResolver().query(
6332                    Account.CONTENT_URI,
6333                    Account.ID_PROJECTION,
6334                    null, null, null);
6335            boolean enable = c != null && c.getCount() > 0;
6336            setServicesEnabled(context, enable);
6337            return enable;
6338        } finally {
6339            if (c != null) {
6340                c.close();
6341            }
6342        }
6343    }
6344
6345    private static void setServicesEnabled(Context context, boolean enabled) {
6346        PackageManager pm = context.getPackageManager();
6347        pm.setComponentEnabledSetting(
6348                new ComponentName(context, AttachmentService.class),
6349                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
6350                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
6351                PackageManager.DONT_KILL_APP);
6352
6353        // Start/stop the various services depending on whether there are any accounts
6354        // TODO: Make sure that the AttachmentService responds to this request as it
6355        // expects a particular set of data in the intents that it receives or it ignores.
6356        startOrStopService(enabled, context, new Intent(context, AttachmentService.class));
6357        final NotificationController controller =
6358                NotificationControllerCreatorHolder.getInstance(context);
6359
6360        if (controller != null) {
6361            controller.watchForMessages();
6362        }
6363    }
6364
6365    /**
6366     * Starts or stops the service as necessary.
6367     * @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
6368     * @param context The context to manage the service with.
6369     * @param intent The intent of the service to be managed.
6370     */
6371    private static void startOrStopService(boolean enabled, Context context, Intent intent) {
6372        if (enabled) {
6373            context.startService(intent);
6374        } else {
6375            context.stopService(intent);
6376        }
6377    }
6378
6379
6380    public static Uri getIncomingSettingsUri(long accountId) {
6381        final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME +
6382                ".ACCOUNT_SETTINGS/incoming/").buildUpon();
6383        IntentUtilities.setAccountId(baseUri, accountId);
6384        return baseUri.build();
6385    }
6386
6387}
6388