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