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