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