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