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