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 
17 package com.android.email.provider;
18 
19 import android.accounts.AccountManager;
20 import android.appwidget.AppWidgetManager;
21 import android.content.ComponentCallbacks;
22 import android.content.ComponentName;
23 import android.content.ContentProvider;
24 import android.content.ContentProviderOperation;
25 import android.content.ContentProviderResult;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.OperationApplicationException;
32 import android.content.PeriodicSync;
33 import android.content.SharedPreferences;
34 import android.content.UriMatcher;
35 import android.content.pm.ActivityInfo;
36 import android.content.pm.PackageManager;
37 import android.content.res.Configuration;
38 import android.content.res.Resources;
39 import android.database.ContentObserver;
40 import android.database.Cursor;
41 import android.database.CursorWrapper;
42 import android.database.DatabaseUtils;
43 import android.database.MatrixCursor;
44 import android.database.MergeCursor;
45 import android.database.sqlite.SQLiteDatabase;
46 import android.database.sqlite.SQLiteException;
47 import android.database.sqlite.SQLiteStatement;
48 import android.net.Uri;
49 import android.os.AsyncTask;
50 import android.os.Binder;
51 import android.os.Build;
52 import android.os.Bundle;
53 import android.os.Handler;
54 import android.os.Handler.Callback;
55 import android.os.Looper;
56 import android.os.Parcel;
57 import android.os.ParcelFileDescriptor;
58 import android.os.RemoteException;
59 import android.provider.BaseColumns;
60 import android.text.TextUtils;
61 import android.text.format.DateUtils;
62 import android.util.Base64;
63 import android.util.Log;
64 import android.util.SparseArray;
65 
66 import com.android.common.content.ProjectionMap;
67 import com.android.email.DebugUtils;
68 import com.android.email.NotificationController;
69 import com.android.email.NotificationControllerCreatorHolder;
70 import com.android.email.Preferences;
71 import com.android.email.R;
72 import com.android.email.SecurityPolicy;
73 import com.android.email.activity.setup.AccountSecurity;
74 import com.android.email.activity.setup.AccountSettingsUtils;
75 import com.android.email.service.AttachmentService;
76 import com.android.email.service.EmailServiceUtils;
77 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
78 import com.android.emailcommon.Logging;
79 import com.android.emailcommon.mail.Address;
80 import com.android.emailcommon.provider.Account;
81 import com.android.emailcommon.provider.Credential;
82 import com.android.emailcommon.provider.EmailContent;
83 import com.android.emailcommon.provider.EmailContent.AccountColumns;
84 import com.android.emailcommon.provider.EmailContent.Attachment;
85 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
86 import com.android.emailcommon.provider.EmailContent.Body;
87 import com.android.emailcommon.provider.EmailContent.BodyColumns;
88 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
89 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
90 import com.android.emailcommon.provider.EmailContent.Message;
91 import com.android.emailcommon.provider.EmailContent.MessageColumns;
92 import com.android.emailcommon.provider.EmailContent.PolicyColumns;
93 import com.android.emailcommon.provider.EmailContent.QuickResponseColumns;
94 import com.android.emailcommon.provider.EmailContent.SyncColumns;
95 import com.android.emailcommon.provider.HostAuth;
96 import com.android.emailcommon.provider.Mailbox;
97 import com.android.emailcommon.provider.MailboxUtilities;
98 import com.android.emailcommon.provider.MessageChangeLogTable;
99 import com.android.emailcommon.provider.MessageMove;
100 import com.android.emailcommon.provider.MessageStateChange;
101 import com.android.emailcommon.provider.Policy;
102 import com.android.emailcommon.provider.QuickResponse;
103 import com.android.emailcommon.service.EmailServiceProxy;
104 import com.android.emailcommon.service.EmailServiceStatus;
105 import com.android.emailcommon.service.IEmailService;
106 import com.android.emailcommon.service.SearchParams;
107 import com.android.emailcommon.utility.AttachmentUtilities;
108 import com.android.emailcommon.utility.EmailAsyncTask;
109 import com.android.emailcommon.utility.IntentUtilities;
110 import com.android.emailcommon.utility.Utility;
111 import com.android.ex.photo.provider.PhotoContract;
112 import com.android.mail.preferences.MailPrefs;
113 import com.android.mail.preferences.MailPrefs.PreferenceKeys;
114 import com.android.mail.providers.Folder;
115 import com.android.mail.providers.FolderList;
116 import com.android.mail.providers.Settings;
117 import com.android.mail.providers.UIProvider;
118 import com.android.mail.providers.UIProvider.AccountCapabilities;
119 import com.android.mail.providers.UIProvider.AccountColumns.SettingsColumns;
120 import com.android.mail.providers.UIProvider.AccountCursorExtraKeys;
121 import com.android.mail.providers.UIProvider.ConversationPriority;
122 import com.android.mail.providers.UIProvider.ConversationSendingState;
123 import com.android.mail.providers.UIProvider.DraftType;
124 import com.android.mail.utils.AttachmentUtils;
125 import com.android.mail.utils.LogTag;
126 import com.android.mail.utils.LogUtils;
127 import com.android.mail.utils.MatrixCursorWithCachedColumns;
128 import com.android.mail.utils.MatrixCursorWithExtra;
129 import com.android.mail.utils.MimeType;
130 import com.android.mail.utils.Utils;
131 import com.android.mail.widget.BaseWidgetProvider;
132 import com.google.common.collect.ImmutableMap;
133 import com.google.common.collect.ImmutableSet;
134 import com.google.common.collect.Sets;
135 
136 import java.io.File;
137 import java.io.FileDescriptor;
138 import java.io.FileNotFoundException;
139 import java.io.FileWriter;
140 import java.io.IOException;
141 import java.io.PrintWriter;
142 import java.util.ArrayList;
143 import java.util.Arrays;
144 import java.util.Collection;
145 import java.util.HashSet;
146 import java.util.List;
147 import java.util.Locale;
148 import java.util.Map;
149 import java.util.Set;
150 import java.util.regex.Pattern;
151 
152 public 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      */
findMatch(Uri uri, String methodName)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 
reconcileAccountsAsync(final Context context)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 
uiUri(String type, long id)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      */
uiUriString(String type, long id)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      */
deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, String foreignTable)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      */
fixParentKeys(SQLiteDatabase db)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
getDatabase(Context context)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      */
initUiProvider()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      */
restoreIfNeeded(Context context, SQLiteDatabase mainDatabase)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
shutdown()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
deleteMessageOrphans(SQLiteDatabase database, String tableName)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
delete(Uri uri, String selection, String[] selectionArgs)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)
getType(Uri uri)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
insert(Uri uri, ContentValues values)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
onCreate()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 
init(final Context context)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      */
checkDatabases()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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 
whereWithId(String id, String selection)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      */
whereWith(String where, String selection)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      */
restoreHostAuth(SQLiteDatabase db, long id)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      */
copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase)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      */
backupAccounts(final Context context, final SQLiteDatabase db)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      */
restoreAccounts(final Context context)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      */
addToMessageMove(final SQLiteDatabase db, final String messageId, final long dstFolderKey)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 
addToMessageStateChange(final SQLiteDatabase db, final String messageId, final int newFlagRead, final int newFlagFavorite)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      */
restartPush(final Context context, final SQLiteDatabase db, final String id)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      */
restartPushForMailbox(final Context context, final SQLiteDatabase db, final ContentValues values, final String accountId)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      */
restartPushForAccount(final Context context, final SQLiteDatabase db, final ContentValues values, final String accountId)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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 
updateSyncStatus(final Bundle extras)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
call(String method, String arg, Bundle extras)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 
deleteBodyFiles(final Context c, final long messageId)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      */
writeBodyFiles(final Context c, final long messageId, final ContentValues cv)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      */
writeBodyFile(final Context c, final long messageId, final String ext, final String content)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      */
getBodyFile(final Context c, final long messageId, final String ext)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
openFile(final Uri uri, final String mode)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      */
getBaseNotificationUri(int match)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      */
sendNotifierChange(Uri baseUri, String op, String id)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 
sendMessageListDataChangedNotification()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 
getBatchNotificationsSet()2519     private Set<Uri> getBatchNotificationsSet() {
2520         return mTLBatchNotifications.get();
2521     }
2522 
setBatchNotificationsSet(Set<Uri> batchNotifications)2523     private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
2524         mTLBatchNotifications.set(batchNotifications);
2525     }
2526 
2527     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)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          */
attachmentChanged(final Context context, final long id, final int flags)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
injectAttachmentService(final EmailAttachmentService attachmentService)2570     public void injectAttachmentService(final EmailAttachmentService attachmentService) {
2571         mAttachmentService =
2572             attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService;
2573     }
2574 
notificationQuery(final Uri uri)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 
mostRecentMessageQuery(Uri uri)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 
getMailboxMessageCount(Uri uri)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      */
getMessageListMap()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      */
getMessageViewMap()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 
getFolderListMap()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      */
getAccountListMap(Context context)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 
getQuickResponseMap()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      */
getAttachmentMap()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      */
genSelect(ProjectionMap map, String[] projection)2966     private static StringBuilder genSelect(ProjectionMap map, String[] projection) {
2967         return genSelect(map, projection, EMPTY_CONTENT_VALUES);
2968     }
2969 
genSelect(ProjectionMap map, String[] projection, ContentValues values)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      */
uriWithId(String type)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      */
uriWithColumn(String type, String columnName)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      */
uriWithFQId(String type, String tableName)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 
MessageQuery(String _query, String _attachmentJson)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      */
genQueryViewMessage(String[] uiProjection, String id)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 
appendConversationInfoColumns(final StringBuilder stringBuilder)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      */
genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly)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      */
getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, long mailboxId, final boolean unseenOnly)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      */
genQueryConversation(String[] uiProjection)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      */
genQueryAccountMailboxes(String[] uiProjection)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      */
genQueryAccountAllMailboxes(String[] uiProjection)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      */
genQueryRecentMailboxes(String[] uiProjection)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 
getFolderCapabilities(EmailServiceInfo info, int mailboxType, long mailboxId)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      */
genQueryMailbox(String[] uiProjection, String id)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 
getExternalUriString(String segment, String account)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 
getExternalUriStringEmail2(String segment, String account)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 
getBits(int bitField)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 
getCapabilities(Context context, final Account account)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      */
genQueryAccount(String[] uiProjection, String id)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      */
combinedUriString(String type, String id)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      */
combinedMailboxId(int type)3705     private static String combinedMailboxId(int type) {
3706         return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type);
3707     }
3708 
getVirtualMailboxId(long accountId, int type)3709     public static long getVirtualMailboxId(long accountId, int type) {
3710         return (accountId << 32) + type;
3711     }
3712 
isVirtualMailbox(long mailboxId)3713     private static boolean isVirtualMailbox(long mailboxId) {
3714         return mailboxId >= 0x100000000L;
3715     }
3716 
isCombinedMailbox(long mailboxId)3717     private static boolean isCombinedMailbox(long mailboxId) {
3718         return (mailboxId >> 32) == COMBINED_ACCOUNT_ID;
3719     }
3720 
getVirtualMailboxAccountId(long mailboxId)3721     private static long getVirtualMailboxAccountId(long mailboxId) {
3722         return mailboxId >> 32;
3723     }
3724 
getVirtualMailboxAccountIdString(long mailboxId)3725     private static String getVirtualMailboxAccountIdString(long mailboxId) {
3726         return Long.toString(mailboxId >> 32);
3727     }
3728 
getVirtualMailboxType(long mailboxId)3729     private static int getVirtualMailboxType(long mailboxId) {
3730         return (int)(mailboxId & 0xF);
3731     }
3732 
addCombinedAccountRow(MatrixCursor mc)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 
getConversationListIcon(MailPrefs mailPrefs)3855     private static int getConversationListIcon(MailPrefs mailPrefs) {
3856         return mailPrefs.getShowSenderImages() ?
3857                 UIProvider.ConversationListIcon.SENDER_IMAGE :
3858                 UIProvider.ConversationListIcon.NONE;
3859     }
3860 
getVirtualMailboxCursor(long mailboxId, String[] projection)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 
getVirtualMailboxRow(long accountId, int mailboxType, String[] projection)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 
uiAccounts(String[] uiProjection, boolean suppressCombined)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 
uiQuickResponseAccount(String[] uiProjection, String account)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 
uiQuickResponseId(String[] uiProjection, String id)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 
uiQuickResponse(String[] uiProjection)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      */
genQueryAttachments(String[] uiProjection, List<String> contentTypeQueryParameters)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      */
genQueryAttachment(String[] uiProjection)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      */
genQueryAttachmentByMessageIDAndCid(String[] uiProjection)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      */
createAttachmentUriColumnSQL()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      */
genQuerySubfolders(String[] uiProjection)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      */
uiFolders(final Uri uri, final String[] uiProjection)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 
uiVirtualMailboxes(final String id, final String[] uiProjection)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      */
defaultRecentFolders(final String id)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      */
getFolder(Context context, long mailboxId)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 
AttachmentsCursor(Context context, Cursor cursor)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
getString(int column)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 
CloseDetectingCursor(Cursor cursor)4342         public CloseDetectingCursor(Cursor cursor) {
4343             super(cursor);
4344         }
4345 
4346         @Override
close()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      */
getUiFolderCursorRowFromMailboxCursorRow( MatrixCursor mc, int projectionLength, Cursor mailboxCursor, int nameColumn, int typeColumn)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      */
remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor, final long accountId, final String[] uiProjection)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 
getFolderListCursor(final Cursor inputCursor, final long accountId, final String[] uiProjection)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      */
getFolderDisplayName(int folderType, String defaultName)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      */
getFolderTypeFromMailboxType(int mailboxType)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      */
getMailboxTypeFromFolderType(int folderType)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      */
folderProjectionFromUiProjection(final String[] uiProjection)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      */
uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly)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.
convertUiAttachmentToAttachment( com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey)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      */
createMailbox(long accountId, int mailboxType)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      */
getMailboxByAccountIdAndType(final long accountId, final int mailboxType)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      */
uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras)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 
uiSaveDraftMessage(final long accountId, final Bundle extras)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 
uiSendDraftMessage(final long accountId, final Bundle extras)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 
putIntegerLongOrBoolean(ContentValues values, String columnName, Object value)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      */
updateTimestamp(final Context context, String id, Uri[] folders)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      */
uiUpdateRecentFolders(Uri uri, ContentValues values)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      */
uiPopulateRecentFolders(Uri uri)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 
uiUpdateAttachment(Uri uri, ContentValues uiValues)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 
uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues)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 
uiUpdateSettings(final Context c, final ContentValues uiValues)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 
markAllSeen(final Context context, final String mailboxId)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 
convertUiMessageValues(Message message, ContentValues values)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 
convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider)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 
getMessageFromLastSegment(Uri uri)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      */
addToSequence(Uri uri, ContentProviderOperation op)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...
uploadsToServer(Context context, Mailbox m)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 
uiUpdateMessage(Uri uri, ContentValues values)5348     private int uiUpdateMessage(Uri uri, ContentValues values) {
5349         return uiUpdateMessage(uri, values, false);
5350     }
5351 
uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync)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      */
handleMessageUpdateNotifications(final Uri uri, final String messageId, final ContentValues values)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      */
uiDeleteMessage(Uri uri)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      */
uiPurgeFolder(Uri uri)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 
pickFolder(Uri uri, int type, int headerId)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 
pickTrashFolder(Uri uri)5568     private int pickTrashFolder(Uri uri) {
5569         return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
5570     }
5571 
pickSentFolder(Uri uri)5572     private int pickSentFolder(Uri uri) {
5573         return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
5574     }
5575 
uiUndo(String[] projection)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 
notifyUIConversation(Uri uri)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      */
notifyUIConversationMailbox(long id)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      */
notifyUIMessage(long id)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      */
notifyUIMessage(String id)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      */
notifyUIAccount(long id)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      */
notifyUIFolder(final String folderId, final long accountId)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 
notifyUIFolder(final long folderId, final long accountId)5703     private void notifyUIFolder(final long folderId, final long accountId) {
5704         notifyUIFolder(Long.toString(folderId), accountId);
5705     }
5706 
notifyUI(final Uri uri, final String id)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 
notifyUI(Uri uri, long id)5717     private void notifyUI(Uri uri, long id) {
5718         notifyUI(uri, Long.toString(id));
5719     }
5720 
getMailbox(final Uri uri)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      */
getAccountManagerAccount(final long accountId)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      */
getAccountManagerAccount(final Context context, final String emailAddress, final String protocol)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      */
updateAccountSyncInterval(final long accountId, final ContentValues values)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      */
startSync(final android.accounts.Account account, final long mailboxId, final int deltaMessageCount)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      */
startSync(final Mailbox mailbox, final int deltaMessageCount)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      */
restartPush(final android.accounts.Account account)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 
uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount)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 
uiFolderLoadMore(final Mailbox mailbox)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      */
getSearchMailbox(long accountId)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 
runSearchQuery(final Context context, final long accountId, final long searchMailboxId)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.
uiSearch(Uri uri, String[] projection)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      */
uiDeleteAccount(Uri uri)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 
uiDeleteAccountData(Uri uri)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      */
decodeEmailAddresses(ContentValues values)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 
deleteAccountData(Context context, long accountId)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 
notifyWidgets(long mailboxId)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
dump(FileDescriptor fd, PrintWriter writer, String[] args)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 
getDelayedSyncHandler()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 
SyncRequestMessage(final String authority, final android.accounts.Account account, final long mailboxId)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
equals(Object o)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
hashCode()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
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)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      */
setServicesEnabledAsync(final Context context)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      */
setServicesEnabledSync(Context context)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 
setServicesEnabled(Context context, boolean enabled)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      */
startOrStopService(boolean enabled, Context context, Intent intent)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 
getIncomingSettingsUri(long accountId)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