1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.email.provider; 18 19 import android.accounts.AccountManager; 20 import android.appwidget.AppWidgetManager; 21 import android.content.ComponentCallbacks; 22 import android.content.ComponentName; 23 import android.content.ContentProvider; 24 import android.content.ContentProviderOperation; 25 import android.content.ContentProviderResult; 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.OperationApplicationException; 32 import android.content.PeriodicSync; 33 import android.content.SharedPreferences; 34 import android.content.UriMatcher; 35 import android.content.pm.ActivityInfo; 36 import android.content.pm.PackageManager; 37 import android.content.res.Configuration; 38 import android.content.res.Resources; 39 import android.database.ContentObserver; 40 import android.database.Cursor; 41 import android.database.CursorWrapper; 42 import android.database.DatabaseUtils; 43 import android.database.MatrixCursor; 44 import android.database.MergeCursor; 45 import android.database.sqlite.SQLiteDatabase; 46 import android.database.sqlite.SQLiteException; 47 import android.database.sqlite.SQLiteStatement; 48 import android.net.Uri; 49 import android.os.AsyncTask; 50 import android.os.Binder; 51 import android.os.Build; 52 import android.os.Bundle; 53 import android.os.Handler; 54 import android.os.Handler.Callback; 55 import android.os.Looper; 56 import android.os.Parcel; 57 import android.os.ParcelFileDescriptor; 58 import android.os.RemoteException; 59 import android.provider.BaseColumns; 60 import android.text.TextUtils; 61 import android.text.format.DateUtils; 62 import android.util.Base64; 63 import android.util.Log; 64 import android.util.SparseArray; 65 66 import com.android.common.content.ProjectionMap; 67 import com.android.email.DebugUtils; 68 import com.android.email.NotificationController; 69 import com.android.email.NotificationControllerCreatorHolder; 70 import com.android.email.Preferences; 71 import com.android.email.R; 72 import com.android.email.SecurityPolicy; 73 import com.android.email.activity.setup.AccountSecurity; 74 import com.android.email.activity.setup.AccountSettingsUtils; 75 import com.android.email.service.AttachmentService; 76 import com.android.email.service.EmailServiceUtils; 77 import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 78 import com.android.emailcommon.Logging; 79 import com.android.emailcommon.mail.Address; 80 import com.android.emailcommon.provider.Account; 81 import com.android.emailcommon.provider.Credential; 82 import com.android.emailcommon.provider.EmailContent; 83 import com.android.emailcommon.provider.EmailContent.AccountColumns; 84 import com.android.emailcommon.provider.EmailContent.Attachment; 85 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 86 import com.android.emailcommon.provider.EmailContent.Body; 87 import com.android.emailcommon.provider.EmailContent.BodyColumns; 88 import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 89 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 90 import com.android.emailcommon.provider.EmailContent.Message; 91 import com.android.emailcommon.provider.EmailContent.MessageColumns; 92 import com.android.emailcommon.provider.EmailContent.PolicyColumns; 93 import com.android.emailcommon.provider.EmailContent.QuickResponseColumns; 94 import com.android.emailcommon.provider.EmailContent.SyncColumns; 95 import com.android.emailcommon.provider.HostAuth; 96 import com.android.emailcommon.provider.Mailbox; 97 import com.android.emailcommon.provider.MailboxUtilities; 98 import com.android.emailcommon.provider.MessageChangeLogTable; 99 import com.android.emailcommon.provider.MessageMove; 100 import com.android.emailcommon.provider.MessageStateChange; 101 import com.android.emailcommon.provider.Policy; 102 import com.android.emailcommon.provider.QuickResponse; 103 import com.android.emailcommon.service.EmailServiceProxy; 104 import com.android.emailcommon.service.EmailServiceStatus; 105 import com.android.emailcommon.service.IEmailService; 106 import com.android.emailcommon.service.SearchParams; 107 import com.android.emailcommon.utility.AttachmentUtilities; 108 import com.android.emailcommon.utility.EmailAsyncTask; 109 import com.android.emailcommon.utility.IntentUtilities; 110 import com.android.emailcommon.utility.Utility; 111 import com.android.ex.photo.provider.PhotoContract; 112 import com.android.mail.preferences.MailPrefs; 113 import com.android.mail.preferences.MailPrefs.PreferenceKeys; 114 import com.android.mail.providers.Folder; 115 import com.android.mail.providers.FolderList; 116 import com.android.mail.providers.Settings; 117 import com.android.mail.providers.UIProvider; 118 import com.android.mail.providers.UIProvider.AccountCapabilities; 119 import com.android.mail.providers.UIProvider.AccountColumns.SettingsColumns; 120 import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 121 import com.android.mail.providers.UIProvider.ConversationPriority; 122 import com.android.mail.providers.UIProvider.ConversationSendingState; 123 import com.android.mail.providers.UIProvider.DraftType; 124 import com.android.mail.utils.AttachmentUtils; 125 import com.android.mail.utils.LogTag; 126 import com.android.mail.utils.LogUtils; 127 import com.android.mail.utils.MatrixCursorWithCachedColumns; 128 import com.android.mail.utils.MatrixCursorWithExtra; 129 import com.android.mail.utils.MimeType; 130 import com.android.mail.utils.Utils; 131 import com.android.mail.widget.BaseWidgetProvider; 132 import com.google.common.collect.ImmutableMap; 133 import com.google.common.collect.ImmutableSet; 134 import com.google.common.collect.Sets; 135 136 import java.io.File; 137 import java.io.FileDescriptor; 138 import java.io.FileNotFoundException; 139 import java.io.FileWriter; 140 import java.io.IOException; 141 import java.io.PrintWriter; 142 import java.util.ArrayList; 143 import java.util.Arrays; 144 import java.util.Collection; 145 import java.util.HashSet; 146 import java.util.List; 147 import java.util.Locale; 148 import java.util.Map; 149 import java.util.Set; 150 import java.util.regex.Pattern; 151 152 public class EmailProvider extends ContentProvider 153 implements SharedPreferences.OnSharedPreferenceChangeListener { 154 155 private static final String TAG = LogTag.getLogTag(); 156 157 // Time to delay upsync requests. 158 public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS; 159 160 public static String EMAIL_APP_MIME_TYPE; 161 162 // exposed for testing 163 public static final String DATABASE_NAME = "EmailProvider.db"; 164 public static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 165 166 // We don't back up to the backup database anymore, just keep this constant here so we can 167 // delete the old backups and trigger a new backup to the account manager 168 @Deprecated 169 private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 170 private static final String ACCOUNT_MANAGER_JSON_TAG = "accountJson"; 171 172 173 private static final String PREFERENCE_FRAGMENT_CLASS_NAME = 174 "com.android.email.activity.setup.AccountSettingsFragment"; 175 /** 176 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 177 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 178 * is NOT the preferred way of getting notification. 179 */ 180 private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 181 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 182 183 private static final String EMAIL_MESSAGE_MIME_TYPE = 184 "vnd.android.cursor.item/email-message"; 185 private static final String EMAIL_ATTACHMENT_MIME_TYPE = 186 "vnd.android.cursor.item/email-attachment"; 187 188 /** Appended to the notification URI for delete operations */ 189 private static final String NOTIFICATION_OP_DELETE = "delete"; 190 /** Appended to the notification URI for insert operations */ 191 private static final String NOTIFICATION_OP_INSERT = "insert"; 192 /** Appended to the notification URI for update operations */ 193 private static final String NOTIFICATION_OP_UPDATE = "update"; 194 195 /** The query string to trigger a folder refresh. */ 196 protected static String QUERY_UIREFRESH = "uirefresh"; 197 198 // Definitions for our queries looking for orphaned messages 199 private static final String[] ORPHANS_PROJECTION 200 = new String[] {MessageColumns._ID, MessageColumns.MAILBOX_KEY}; 201 private static final int ORPHANS_ID = 0; 202 private static final int ORPHANS_MAILBOX_KEY = 1; 203 204 private static final String WHERE_ID = BaseColumns._ID + "=?"; 205 206 private static final int ACCOUNT_BASE = 0; 207 private static final int ACCOUNT = ACCOUNT_BASE; 208 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 209 private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 2; 210 private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 3; 211 private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 4; 212 213 private static final int MAILBOX_BASE = 0x1000; 214 private static final int MAILBOX = MAILBOX_BASE; 215 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 216 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2; 217 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3; 218 private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4; 219 220 private static final int MESSAGE_BASE = 0x2000; 221 private static final int MESSAGE = MESSAGE_BASE; 222 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 223 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 224 private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3; 225 private static final int MESSAGE_MOVE = MESSAGE_BASE + 4; 226 private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5; 227 228 private static final int ATTACHMENT_BASE = 0x3000; 229 private static final int ATTACHMENT = ATTACHMENT_BASE; 230 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 231 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 232 private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3; 233 234 private static final int HOSTAUTH_BASE = 0x4000; 235 private static final int HOSTAUTH = HOSTAUTH_BASE; 236 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 237 238 private static final int UPDATED_MESSAGE_BASE = 0x5000; 239 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 240 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 241 242 private static final int DELETED_MESSAGE_BASE = 0x6000; 243 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 244 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 245 246 private static final int POLICY_BASE = 0x7000; 247 private static final int POLICY = POLICY_BASE; 248 private static final int POLICY_ID = POLICY_BASE + 1; 249 250 private static final int QUICK_RESPONSE_BASE = 0x8000; 251 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 252 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 253 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 254 255 private static final int UI_BASE = 0x9000; 256 private static final int UI_FOLDERS = UI_BASE; 257 private static final int UI_SUBFOLDERS = UI_BASE + 1; 258 private static final int UI_MESSAGES = UI_BASE + 2; 259 private static final int UI_MESSAGE = UI_BASE + 3; 260 private static final int UI_UNDO = UI_BASE + 4; 261 private static final int UI_FOLDER_REFRESH = UI_BASE + 5; 262 private static final int UI_FOLDER = UI_BASE + 6; 263 private static final int UI_ACCOUNT = UI_BASE + 7; 264 private static final int UI_ACCTS = UI_BASE + 8; 265 private static final int UI_ATTACHMENTS = UI_BASE + 9; 266 private static final int UI_ATTACHMENT = UI_BASE + 10; 267 private static final int UI_ATTACHMENT_BY_CID = UI_BASE + 11; 268 private static final int UI_SEARCH = UI_BASE + 12; 269 private static final int UI_ACCOUNT_DATA = UI_BASE + 13; 270 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 14; 271 private static final int UI_CONVERSATION = UI_BASE + 15; 272 private static final int UI_RECENT_FOLDERS = UI_BASE + 16; 273 private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 17; 274 private static final int UI_FULL_FOLDERS = UI_BASE + 18; 275 private static final int UI_ALL_FOLDERS = UI_BASE + 19; 276 private static final int UI_PURGE_FOLDER = UI_BASE + 20; 277 private static final int UI_INBOX = UI_BASE + 21; 278 private static final int UI_ACCTSETTINGS = UI_BASE + 22; 279 280 private static final int BODY_BASE = 0xA000; 281 private static final int BODY = BODY_BASE; 282 private static final int BODY_ID = BODY_BASE + 1; 283 private static final int BODY_HTML = BODY_BASE + 2; 284 private static final int BODY_TEXT = BODY_BASE + 3; 285 286 private static final int CREDENTIAL_BASE = 0xB000; 287 private static final int CREDENTIAL = CREDENTIAL_BASE; 288 private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1; 289 290 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 291 292 private static final SparseArray<String> TABLE_NAMES; 293 static { 294 SparseArray<String> array = new SparseArray<String>(11); 295 array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME); 296 array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME); 297 array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME); 298 array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME); 299 array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME); 300 array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME); 301 array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME); 302 array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME); 303 array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME); 304 array.put(UI_BASE >> BASE_SHIFT, null); 305 array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); 306 array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME); 307 TABLE_NAMES = array; 308 } 309 310 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 311 312 /** 313 * Functions which manipulate the database connection or files synchronize on this. 314 * It's static because there can be multiple provider objects. 315 * TODO: Do we actually need to synchronize across all DB access, not just connection creation? 316 */ 317 private static final Object sDatabaseLock = new Object(); 318 319 /** 320 * Let's only generate these SQL strings once, as they are used frequently 321 * Note that this isn't relevant for table creation strings, since they are used only once 322 */ 323 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 324 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 325 BaseColumns._ID + '='; 326 327 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 328 Message.UPDATED_TABLE_NAME + " where " + BaseColumns._ID + '='; 329 330 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 331 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 332 BaseColumns._ID + '='; 333 334 private static final String ORPHAN_BODY_MESSAGE_ID_SELECT = 335 "select " + BodyColumns.MESSAGE_KEY + " from " + Body.TABLE_NAME + 336 " except select " + BaseColumns._ID + " from " + Message.TABLE_NAME; 337 338 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 339 " where " + BodyColumns.MESSAGE_KEY + " in " + '(' + ORPHAN_BODY_MESSAGE_ID_SELECT + ')'; 340 341 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 342 " where " + BodyColumns.MESSAGE_KEY + '='; 343 344 private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); 345 346 private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 347 348 // For undo handling 349 private int mLastSequence = -1; 350 private final ArrayList<ContentProviderOperation> mLastSequenceOps = 351 new ArrayList<ContentProviderOperation>(); 352 353 // Query parameter indicating the command came from UIProvider 354 private static final String IS_UIPROVIDER = "is_uiprovider"; 355 356 private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status"; 357 358 private static final String[] MIME_TYPE_PROJECTION = new String[]{AttachmentColumns.MIME_TYPE}; 359 360 private static final String[] CACHED_FILE_QUERY_PROJECTION = new String[] 361 { AttachmentColumns._ID, AttachmentColumns.FILENAME, AttachmentColumns.SIZE, 362 AttachmentColumns.CONTENT_URI }; 363 364 /** 365 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 366 * @param uri the Uri to match 367 * @return the match value 368 */ 369 private static int findMatch(Uri uri, String methodName) { 370 int match = sURIMatcher.match(uri); 371 if (match < 0) { 372 throw new IllegalArgumentException("Unknown uri: " + uri); 373 } else if (Logging.LOGD) { 374 LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 375 } 376 return match; 377 } 378 379 // exposed for testing 380 public static Uri INTEGRITY_CHECK_URI; 381 382 public static Uri ACCOUNT_BACKUP_URI; 383 private static Uri FOLDER_STATUS_URI; 384 385 private SQLiteDatabase mDatabase; 386 private SQLiteDatabase mBodyDatabase; 387 388 private Handler mDelayedSyncHandler; 389 private final Set<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>(); 390 391 private static void reconcileAccountsAsync(final Context context) { 392 if (context.getResources().getBoolean(R.bool.reconcile_accounts)) { 393 EmailAsyncTask.runAsyncParallel(new Runnable() { 394 @Override 395 public void run() { 396 AccountReconciler.reconcileAccounts(context); 397 } 398 }); 399 } 400 } 401 402 public static Uri uiUri(String type, long id) { 403 return Uri.parse(uiUriString(type, id)); 404 } 405 406 /** 407 * Creates a URI string from a database ID (guaranteed to be unique). 408 * @param type of the resource: uifolder, message, etc. 409 * @param id the id of the resource. 410 * @return uri string 411 */ 412 public static String uiUriString(String type, long id) { 413 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 414 } 415 416 /** 417 * Orphan record deletion utility. Generates a sqlite statement like: 418 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 419 * Exposed for testing. 420 * @param db the EmailProvider database 421 * @param table the table whose orphans are to be removed 422 * @param column the column deletion will be based on 423 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 424 * @param foreignTable the foreign table 425 */ 426 public static void deleteUnlinked(SQLiteDatabase db, String table, String column, 427 String foreignColumn, String foreignTable) { 428 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 429 foreignTable + ")", null); 430 if (count > 0) { 431 LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table); 432 } 433 } 434 435 436 /** 437 * Make sure that parentKeys match with parentServerId. 438 * When we sync folders, we do two passes: First to create the mailbox rows, and second 439 * to set the parentKeys. Two passes are needed because we won't know the parent's Id 440 * until that row is inserted, and the order in which the rows are given is arbitrary. 441 * If we crash while this operation is in progress, the parent keys can be left uninitialized. 442 * @param db SQLiteDatabase to modify 443 */ 444 private void fixParentKeys(SQLiteDatabase db) { 445 LogUtils.d(TAG, "Fixing parent keys"); 446 447 // Update the parentKey for each mailbox row to match the _id of the row whose 448 // serverId matches our parentServerId. This will leave parentKey blank for any 449 // row that does not have a parentServerId 450 451 // This is kind of a confusing sql statement, so here's the actual text of it, 452 // for reference: 453 // 454 // update mailbox set parentKey = (select _id from mailbox as b where 455 // mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and 456 // mailbox.accountKey=b.accountKey) 457 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "=" 458 + "(select " + Mailbox._ID + " from " + Mailbox.TABLE_NAME + " as b where " 459 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "=" 460 + "b." + MailboxColumns.SERVER_ID + " and " 461 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and " 462 + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY 463 + "=b." + Mailbox.ACCOUNT_KEY + ")"); 464 465 // Top level folders can still have uninitialized parent keys. Update these 466 // to indicate that the parent is -1. 467 // 468 // update mailbox set parentKey = -1 where parentKey=0 or parentKey is null; 469 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY 470 + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY 471 + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY 472 + " is null"); 473 474 } 475 476 // exposed for testing 477 public SQLiteDatabase getDatabase(Context context) { 478 synchronized (sDatabaseLock) { 479 // Always return the cached database, if we've got one 480 if (mDatabase != null) { 481 return mDatabase; 482 } 483 484 // Whenever we create or re-cache the databases, make sure that we haven't lost one 485 // to corruption 486 checkDatabases(); 487 488 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 489 mDatabase = helper.getWritableDatabase(); 490 DBHelper.BodyDatabaseHelper bodyHelper = 491 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 492 mBodyDatabase = bodyHelper.getWritableDatabase(); 493 if (mBodyDatabase != null) { 494 String bodyFileName = mBodyDatabase.getPath(); 495 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 496 } 497 498 // Restore accounts if the database is corrupted... 499 restoreIfNeeded(context, mDatabase); 500 // Check for any orphaned Messages in the updated/deleted tables 501 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 502 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 503 // Delete orphaned mailboxes/messages/policies (account no longer exists) 504 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, 505 AccountColumns._ID, Account.TABLE_NAME); 506 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, 507 AccountColumns._ID, Account.TABLE_NAME); 508 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns._ID, 509 AccountColumns.POLICY_KEY, Account.TABLE_NAME); 510 fixParentKeys(mDatabase); 511 initUiProvider(); 512 return mDatabase; 513 } 514 } 515 516 /** 517 * Perform startup actions related to UI 518 */ 519 private void initUiProvider() { 520 // Clear mailbox sync status 521 mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + 522 "=" + UIProvider.SyncStatus.NO_SYNC); 523 } 524 525 /** 526 * Restore user Account and HostAuth data from our backup database 527 */ 528 private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 529 if (DebugUtils.DEBUG) { 530 LogUtils.w(TAG, "restoreIfNeeded..."); 531 } 532 // Check for legacy backup 533 String legacyBackup = Preferences.getLegacyBackupPreference(context); 534 // If there's a legacy backup, create a new-style backup and delete the legacy backup 535 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 536 // corrupt, oh well... 537 if (!TextUtils.isEmpty(legacyBackup)) { 538 backupAccounts(context, mainDatabase); 539 Preferences.clearLegacyBackupPreference(context); 540 LogUtils.w(TAG, "Created new EmailProvider backup database"); 541 return; 542 } 543 544 // If there's a backup database (old style) delete it and trigger an account manager backup. 545 // Roughly the same comment as above applies 546 final File backupDb = context.getDatabasePath(BACKUP_DATABASE_NAME); 547 if (backupDb.exists()) { 548 backupAccounts(context, mainDatabase); 549 context.deleteDatabase(BACKUP_DATABASE_NAME); 550 LogUtils.w(TAG, "Migrated from backup database to account manager"); 551 return; 552 } 553 554 // If we have accounts, we're done 555 if (DatabaseUtils.longForQuery(mainDatabase, 556 "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )", 557 EmailContent.ID_PROJECTION) > 0) { 558 if (DebugUtils.DEBUG) { 559 LogUtils.w(TAG, "restoreIfNeeded: Account exists."); 560 } 561 return; 562 } 563 564 restoreAccounts(context); 565 } 566 567 /** {@inheritDoc} */ 568 @Override 569 public void shutdown() { 570 if (mDatabase != null) { 571 mDatabase.close(); 572 mDatabase = null; 573 } 574 if (mBodyDatabase != null) { 575 mBodyDatabase.close(); 576 mBodyDatabase = null; 577 } 578 } 579 580 // exposed for testing 581 public static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 582 if (database != null) { 583 // We'll look at all of the items in the table; there won't be many typically 584 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 585 // Usually, there will be nothing in these tables, so make a quick check 586 try { 587 if (c.getCount() == 0) return; 588 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 589 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 590 ArrayList<Long> deleteList = new ArrayList<Long>(); 591 String[] bindArray = new String[1]; 592 while (c.moveToNext()) { 593 // Get the mailbox key and see if we've already found this mailbox 594 // If so, we're fine 595 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 596 // If we already know this mailbox doesn't exist, mark the message for deletion 597 if (notFoundMailboxes.contains(mailboxId)) { 598 deleteList.add(c.getLong(ORPHANS_ID)); 599 // If we don't know about this mailbox, we'll try to find it 600 } else if (!foundMailboxes.contains(mailboxId)) { 601 bindArray[0] = Long.toString(mailboxId); 602 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 603 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 604 try { 605 // If it exists, we'll add it to the "found" mailboxes 606 if (boxCursor.moveToFirst()) { 607 foundMailboxes.add(mailboxId); 608 // Otherwise, we'll add to "not found" and mark the message for deletion 609 } else { 610 notFoundMailboxes.add(mailboxId); 611 deleteList.add(c.getLong(ORPHANS_ID)); 612 } 613 } finally { 614 boxCursor.close(); 615 } 616 } 617 } 618 // Now, delete the orphan messages 619 for (long messageId: deleteList) { 620 bindArray[0] = Long.toString(messageId); 621 database.delete(tableName, WHERE_ID, bindArray); 622 } 623 } finally { 624 c.close(); 625 } 626 } 627 } 628 629 @Override 630 public int delete(Uri uri, String selection, String[] selectionArgs) { 631 Log.d(TAG, "Delete: " + uri); 632 final int match = findMatch(uri, "delete"); 633 final Context context = getContext(); 634 // Pick the correct database for this operation 635 // If we're in a transaction already (which would happen during applyBatch), then the 636 // body database is already attached to the email database and any attempt to use the 637 // body database directly will result in a SQLiteException (the database is locked) 638 final SQLiteDatabase db = getDatabase(context); 639 final int table = match >> BASE_SHIFT; 640 String id = "0"; 641 boolean messageDeletion = false; 642 643 final String tableName = TABLE_NAMES.valueAt(table); 644 int result = -1; 645 646 try { 647 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 648 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 649 notifyUIConversation(uri); 650 } 651 } 652 switch (match) { 653 case UI_MESSAGE: 654 return uiDeleteMessage(uri); 655 case UI_ACCOUNT_DATA: 656 return uiDeleteAccountData(uri); 657 case UI_ACCOUNT: 658 return uiDeleteAccount(uri); 659 case UI_PURGE_FOLDER: 660 return uiPurgeFolder(uri); 661 case MESSAGE_SELECTION: 662 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 663 selectionArgs, null, null, null); 664 try { 665 if (findCursor.moveToFirst()) { 666 return delete(ContentUris.withAppendedId( 667 Message.CONTENT_URI, 668 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 669 null, null); 670 } else { 671 return 0; 672 } 673 } finally { 674 findCursor.close(); 675 } 676 // These are cases in which one or more Messages might get deleted, either by 677 // cascade or explicitly 678 case MAILBOX_ID: 679 case MAILBOX: 680 case ACCOUNT_ID: 681 case ACCOUNT: 682 case MESSAGE: 683 case SYNCED_MESSAGE_ID: 684 case MESSAGE_ID: 685 // Handle lost Body records here, since this cannot be done in a trigger 686 // The process is: 687 // 1) Begin a transaction, ensuring that both databases are affected atomically 688 // 2) Do the requested deletion, with cascading deletions handled in triggers 689 // 3) End the transaction, committing all changes atomically 690 // 691 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 692 messageDeletion = true; 693 db.beginTransaction(); 694 break; 695 } 696 switch (match) { 697 case BODY_ID: 698 case DELETED_MESSAGE_ID: 699 case SYNCED_MESSAGE_ID: 700 case MESSAGE_ID: 701 case UPDATED_MESSAGE_ID: 702 case ATTACHMENT_ID: 703 case MAILBOX_ID: 704 case ACCOUNT_ID: 705 case HOSTAUTH_ID: 706 case POLICY_ID: 707 case QUICK_RESPONSE_ID: 708 case CREDENTIAL_ID: 709 id = uri.getPathSegments().get(1); 710 if (match == SYNCED_MESSAGE_ID) { 711 // For synced messages, first copy the old message to the deleted table and 712 // delete it from the updated table (in case it was updated first) 713 // Note that this is all within a transaction, for atomicity 714 db.execSQL(DELETED_MESSAGE_INSERT + id); 715 db.execSQL(UPDATED_MESSAGE_DELETE + id); 716 } 717 718 final long accountId; 719 if (match == MAILBOX_ID) { 720 accountId = Mailbox.getAccountIdForMailbox(context, id); 721 } else { 722 accountId = Account.NO_ACCOUNT; 723 } 724 725 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 726 727 if (match == ACCOUNT_ID) { 728 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 729 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 730 } else if (match == MAILBOX_ID) { 731 notifyUIFolder(id, accountId); 732 } else if (match == ATTACHMENT_ID) { 733 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 734 } 735 break; 736 case ATTACHMENTS_MESSAGE_ID: 737 // All attachments for the given message 738 id = uri.getPathSegments().get(2); 739 result = db.delete(tableName, 740 whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection), 741 selectionArgs); 742 break; 743 744 case BODY: 745 case MESSAGE: 746 case DELETED_MESSAGE: 747 case UPDATED_MESSAGE: 748 case ATTACHMENT: 749 case MAILBOX: 750 case ACCOUNT: 751 case HOSTAUTH: 752 case POLICY: 753 result = db.delete(tableName, selection, selectionArgs); 754 break; 755 case MESSAGE_MOVE: 756 db.delete(MessageMove.TABLE_NAME, selection, selectionArgs); 757 break; 758 case MESSAGE_STATE_CHANGE: 759 db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs); 760 break; 761 default: 762 throw new IllegalArgumentException("Unknown URI " + uri); 763 } 764 if (messageDeletion) { 765 if (match == MESSAGE_ID) { 766 // Delete the Body record associated with the deleted message 767 final long messageId = Long.valueOf(id); 768 try { 769 deleteBodyFiles(context, messageId); 770 } catch (final IllegalStateException e) { 771 LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies"); 772 } 773 db.execSQL(DELETE_BODY + id); 774 } else { 775 // Delete any orphaned Body records 776 final Cursor orphans = db.rawQuery(ORPHAN_BODY_MESSAGE_ID_SELECT, null); 777 try { 778 while (orphans.moveToNext()) { 779 final long messageId = orphans.getLong(0); 780 try { 781 deleteBodyFiles(context, messageId); 782 } catch (final IllegalStateException e) { 783 LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies"); 784 } 785 } 786 } finally { 787 orphans.close(); 788 } 789 db.execSQL(DELETE_ORPHAN_BODIES); 790 } 791 db.setTransactionSuccessful(); 792 } 793 } catch (SQLiteException e) { 794 checkDatabases(); 795 throw e; 796 } finally { 797 if (messageDeletion) { 798 db.endTransaction(); 799 } 800 } 801 802 // Notify all notifier cursors 803 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 804 805 // Notify all email content cursors 806 notifyUI(EmailContent.CONTENT_URI, null); 807 return result; 808 } 809 810 @Override 811 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 812 public String getType(Uri uri) { 813 int match = findMatch(uri, "getType"); 814 switch (match) { 815 case BODY_ID: 816 return "vnd.android.cursor.item/email-body"; 817 case BODY: 818 return "vnd.android.cursor.dir/email-body"; 819 case UPDATED_MESSAGE_ID: 820 case MESSAGE_ID: 821 // NOTE: According to the framework folks, we're supposed to invent mime types as 822 // a way of passing information to drag & drop recipients. 823 // If there's a mailboxId parameter in the url, we respond with a mime type that 824 // has -n appended, where n is the mailboxId of the message. The drag & drop code 825 // uses this information to know not to allow dragging the item to its own mailbox 826 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 827 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 828 if (mailboxId != null) { 829 mimeType += "-" + mailboxId; 830 } 831 return mimeType; 832 case UPDATED_MESSAGE: 833 case MESSAGE: 834 return "vnd.android.cursor.dir/email-message"; 835 case MAILBOX: 836 return "vnd.android.cursor.dir/email-mailbox"; 837 case MAILBOX_ID: 838 return "vnd.android.cursor.item/email-mailbox"; 839 case ACCOUNT: 840 return "vnd.android.cursor.dir/email-account"; 841 case ACCOUNT_ID: 842 return "vnd.android.cursor.item/email-account"; 843 case ATTACHMENTS_MESSAGE_ID: 844 case ATTACHMENT: 845 return "vnd.android.cursor.dir/email-attachment"; 846 case ATTACHMENT_ID: 847 return EMAIL_ATTACHMENT_MIME_TYPE; 848 case HOSTAUTH: 849 return "vnd.android.cursor.dir/email-hostauth"; 850 case HOSTAUTH_ID: 851 return "vnd.android.cursor.item/email-hostauth"; 852 case ATTACHMENTS_CACHED_FILE_ACCESS: { 853 SQLiteDatabase db = getDatabase(getContext()); 854 Cursor c = db.query(Attachment.TABLE_NAME, MIME_TYPE_PROJECTION, 855 AttachmentColumns.CACHED_FILE + "=?", new String[]{uri.toString()}, 856 null, null, null, null); 857 try { 858 if (c != null && c.moveToFirst()) { 859 return c.getString(0); 860 } else { 861 return null; 862 } 863 } finally { 864 if (c != null) { 865 c.close(); 866 } 867 } 868 } 869 default: 870 return null; 871 } 872 } 873 874 // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI 875 // as the base because that gets spammed. 876 // These can't be statically initialized because they depend on EmailContent.AUTHORITY 877 private static Uri UIPROVIDER_CONVERSATION_NOTIFIER; 878 private static Uri UIPROVIDER_FOLDER_NOTIFIER; 879 private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER; 880 private static Uri UIPROVIDER_ACCOUNT_NOTIFIER; 881 // Not currently used 882 //public static Uri UIPROVIDER_SETTINGS_NOTIFIER; 883 private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER; 884 private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER; 885 private static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER; 886 private static Uri UIPROVIDER_MESSAGE_NOTIFIER; 887 private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER; 888 889 @Override 890 public Uri insert(Uri uri, ContentValues values) { 891 Log.d(TAG, "Insert: " + uri); 892 final int match = findMatch(uri, "insert"); 893 final Context context = getContext(); 894 895 // See the comment at delete(), above 896 final SQLiteDatabase db = getDatabase(context); 897 final int table = match >> BASE_SHIFT; 898 String id = "0"; 899 long longId; 900 901 // We do NOT allow setting of unreadCount/messageCount via the provider 902 // These columns are maintained via triggers 903 if (match == MAILBOX_ID || match == MAILBOX) { 904 values.put(MailboxColumns.UNREAD_COUNT, 0); 905 values.put(MailboxColumns.MESSAGE_COUNT, 0); 906 } 907 908 final Uri resultUri; 909 910 try { 911 switch (match) { 912 case BODY: 913 final ContentValues dbValues = new ContentValues(values); 914 // Prune out the content we don't want in the DB 915 dbValues.remove(BodyColumns.HTML_CONTENT); 916 dbValues.remove(BodyColumns.TEXT_CONTENT); 917 // TODO: move this to the message table 918 longId = db.insert(Body.TABLE_NAME, "foo", dbValues); 919 resultUri = ContentUris.withAppendedId(uri, longId); 920 // Write content to the filesystem where appropriate 921 // This will look less ugly once the body table is folded into the message table 922 // and we can just use longId instead 923 if (!values.containsKey(BodyColumns.MESSAGE_KEY)) { 924 throw new IllegalArgumentException( 925 "Cannot insert body without MESSAGE_KEY"); 926 } 927 final long messageId = values.getAsLong(BodyColumns.MESSAGE_KEY); 928 // Ensure that no pre-existing body files contaminate the message 929 deleteBodyFiles(context, messageId); 930 writeBodyFiles(getContext(), messageId, values); 931 break; 932 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 933 // or DELETED_MESSAGE; see the comment below for details 934 case UPDATED_MESSAGE: 935 case DELETED_MESSAGE: 936 case MESSAGE: 937 decodeEmailAddresses(values); 938 case ATTACHMENT: 939 case MAILBOX: 940 case ACCOUNT: 941 case HOSTAUTH: 942 case CREDENTIAL: 943 case POLICY: 944 case QUICK_RESPONSE: 945 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 946 resultUri = ContentUris.withAppendedId(uri, longId); 947 switch(match) { 948 case MESSAGE: 949 final long mailboxId = values.getAsLong(MessageColumns.MAILBOX_KEY); 950 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 951 notifyUIConversationMailbox(mailboxId); 952 } 953 notifyUIFolder(mailboxId, values.getAsLong(MessageColumns.ACCOUNT_KEY)); 954 break; 955 case MAILBOX: 956 if (values.containsKey(MailboxColumns.TYPE)) { 957 if (values.getAsInteger(MailboxColumns.TYPE) < 958 Mailbox.TYPE_NOT_EMAIL) { 959 // Notify the account when a new mailbox is added 960 final Long accountId = 961 values.getAsLong(MailboxColumns.ACCOUNT_KEY); 962 if (accountId != null && accountId > 0) { 963 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); 964 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 965 } 966 } 967 } 968 break; 969 case ACCOUNT: 970 updateAccountSyncInterval(longId, values); 971 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 972 notifyUIAccount(longId); 973 } 974 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 975 break; 976 case UPDATED_MESSAGE: 977 case DELETED_MESSAGE: 978 throw new IllegalArgumentException("Unknown URL " + uri); 979 case ATTACHMENT: 980 int flags = 0; 981 if (values.containsKey(AttachmentColumns.FLAGS)) { 982 flags = values.getAsInteger(AttachmentColumns.FLAGS); 983 } 984 // Report all new attachments to the download service 985 if (TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { 986 LogUtils.w(TAG, new Throwable(), "attachment with blank location"); 987 } 988 mAttachmentService.attachmentChanged(getContext(), longId, flags); 989 break; 990 } 991 break; 992 case QUICK_RESPONSE_ACCOUNT_ID: 993 longId = Long.parseLong(uri.getPathSegments().get(2)); 994 values.put(QuickResponseColumns.ACCOUNT_KEY, longId); 995 return insert(QuickResponse.CONTENT_URI, values); 996 case MAILBOX_ID: 997 // This implies adding a message to a mailbox 998 // Hmm, a problem here is that we can't link the account as well, so it must be 999 // already in the values... 1000 longId = Long.parseLong(uri.getPathSegments().get(1)); 1001 values.put(MessageColumns.MAILBOX_KEY, longId); 1002 return insert(Message.CONTENT_URI, values); // Recurse 1003 case MESSAGE_ID: 1004 // This implies adding an attachment to a message. 1005 id = uri.getPathSegments().get(1); 1006 longId = Long.parseLong(id); 1007 values.put(AttachmentColumns.MESSAGE_KEY, longId); 1008 return insert(Attachment.CONTENT_URI, values); // Recurse 1009 case ACCOUNT_ID: 1010 // This implies adding a mailbox to an account. 1011 longId = Long.parseLong(uri.getPathSegments().get(1)); 1012 values.put(MailboxColumns.ACCOUNT_KEY, longId); 1013 return insert(Mailbox.CONTENT_URI, values); // Recurse 1014 case ATTACHMENTS_MESSAGE_ID: 1015 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 1016 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1017 break; 1018 default: 1019 throw new IllegalArgumentException("Unknown URL " + uri); 1020 } 1021 } catch (SQLiteException e) { 1022 checkDatabases(); 1023 throw e; 1024 } 1025 1026 // Notify all notifier cursors 1027 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1028 1029 // Notify all existing cursors. 1030 notifyUI(EmailContent.CONTENT_URI, null); 1031 return resultUri; 1032 } 1033 1034 @Override 1035 public boolean onCreate() { 1036 Context context = getContext(); 1037 EmailContent.init(context); 1038 init(context); 1039 DebugUtils.init(context); 1040 // Do this last, so that EmailContent/EmailProvider are initialized 1041 setServicesEnabledAsync(context); 1042 reconcileAccountsAsync(context); 1043 1044 // Update widgets 1045 final Intent updateAllWidgetsIntent = 1046 new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED); 1047 updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true); 1048 updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type)); 1049 context.sendBroadcast(updateAllWidgetsIntent); 1050 1051 // The combined account name changes on locale changes 1052 final Configuration oldConfiguration = 1053 new Configuration(context.getResources().getConfiguration()); 1054 context.registerComponentCallbacks(new ComponentCallbacks() { 1055 @Override 1056 public void onConfigurationChanged(Configuration configuration) { 1057 int delta = oldConfiguration.updateFrom(configuration); 1058 if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) { 1059 notifyUIAccount(COMBINED_ACCOUNT_ID); 1060 } 1061 } 1062 1063 @Override 1064 public void onLowMemory() {} 1065 }); 1066 1067 MailPrefs.get(context).registerOnSharedPreferenceChangeListener(this); 1068 1069 return false; 1070 } 1071 1072 private static void init(final Context context) { 1073 // Synchronize on the matcher rather than the class object to minimize risk of contention 1074 // & deadlock. 1075 synchronized (sURIMatcher) { 1076 // We use the existence of this variable as indicative of whether this function has 1077 // already run. 1078 if (INTEGRITY_CHECK_URI != null) { 1079 return; 1080 } 1081 INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + 1082 "/integrityCheck"); 1083 ACCOUNT_BACKUP_URI = 1084 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 1085 FOLDER_STATUS_URI = 1086 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 1087 EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); 1088 1089 final String uiNotificationAuthority = 1090 EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications"; 1091 UIPROVIDER_CONVERSATION_NOTIFIER = 1092 Uri.parse("content://" + uiNotificationAuthority + "/uimessages"); 1093 UIPROVIDER_FOLDER_NOTIFIER = 1094 Uri.parse("content://" + uiNotificationAuthority + "/uifolder"); 1095 UIPROVIDER_FOLDERLIST_NOTIFIER = 1096 Uri.parse("content://" + uiNotificationAuthority + "/uifolders"); 1097 UIPROVIDER_ACCOUNT_NOTIFIER = 1098 Uri.parse("content://" + uiNotificationAuthority + "/uiaccount"); 1099 // Not currently used 1100 /* UIPROVIDER_SETTINGS_NOTIFIER = 1101 Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/ 1102 UIPROVIDER_ATTACHMENT_NOTIFIER = 1103 Uri.parse("content://" + uiNotificationAuthority + "/uiattachment"); 1104 UIPROVIDER_ATTACHMENTS_NOTIFIER = 1105 Uri.parse("content://" + uiNotificationAuthority + "/uiattachments"); 1106 UIPROVIDER_ALL_ACCOUNTS_NOTIFIER = 1107 Uri.parse("content://" + uiNotificationAuthority + "/uiaccts"); 1108 UIPROVIDER_MESSAGE_NOTIFIER = 1109 Uri.parse("content://" + uiNotificationAuthority + "/uimessage"); 1110 UIPROVIDER_RECENT_FOLDERS_NOTIFIER = 1111 Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders"); 1112 1113 // All accounts 1114 sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 1115 // A specific account 1116 // insert into this URI causes a mailbox to be added to the account 1117 sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 1118 sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); 1119 1120 // All mailboxes 1121 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 1122 // A specific mailbox 1123 // insert into this URI causes a message to be added to the mailbox 1124 // ** NOTE For now, the accountKey must be set manually in the values! 1125 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID); 1126 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", 1127 MAILBOX_NOTIFICATION); 1128 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 1129 MAILBOX_MOST_RECENT_MESSAGE); 1130 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT); 1131 1132 // All messages 1133 sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 1134 // A specific message 1135 // insert into this URI causes an attachment to be added to the message 1136 sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 1137 1138 // A specific attachment 1139 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 1140 // A specific attachment (the header information) 1141 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 1142 // The attachments of a specific message (query only) (insert & delete TBD) 1143 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 1144 ATTACHMENTS_MESSAGE_ID); 1145 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile", 1146 ATTACHMENTS_CACHED_FILE_ACCESS); 1147 1148 // All mail bodies 1149 sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY); 1150 // A specific mail body 1151 sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 1152 // A specific HTML body part, for openFile 1153 sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML); 1154 // A specific text body part, for openFile 1155 sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT); 1156 1157 // All hostauth records 1158 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 1159 // A specific hostauth 1160 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); 1161 1162 // All credential records 1163 sURIMatcher.addURI(EmailContent.AUTHORITY, "credential", CREDENTIAL); 1164 // A specific credential 1165 sURIMatcher.addURI(EmailContent.AUTHORITY, "credential/*", CREDENTIAL_ID); 1166 1167 /** 1168 * THIS URI HAS SPECIAL SEMANTICS 1169 * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK 1170 * TO A SERVER VIA A SYNC ADAPTER 1171 */ 1172 sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 1173 sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); 1174 1175 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE); 1176 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH, 1177 MESSAGE_STATE_CHANGE); 1178 1179 /** 1180 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 1181 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 1182 * BY THE UI APPLICATION 1183 */ 1184 // All deleted messages 1185 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 1186 // A specific deleted message 1187 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 1188 1189 // All updated messages 1190 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 1191 // A specific updated message 1192 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 1193 1194 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 1195 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 1196 1197 // All quick responses 1198 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 1199 // A specific quick response 1200 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 1201 // All quick responses associated with a particular account id 1202 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 1203 QUICK_RESPONSE_ACCOUNT_ID); 1204 1205 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 1206 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS); 1207 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); 1208 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 1209 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 1210 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 1211 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 1212 sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH); 1213 // We listen to everything trailing uifolder/ since there might be an appVersion 1214 // as in Utils.appendVersionQueryParameter(). 1215 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER); 1216 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiinbox/#", UI_INBOX); 1217 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 1218 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 1219 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiacctsettings", UI_ACCTSETTINGS); 1220 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 1221 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 1222 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachmentbycid/#/*", 1223 UI_ATTACHMENT_BY_CID); 1224 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 1225 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 1226 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 1227 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 1228 sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 1229 sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", 1230 UI_DEFAULT_RECENT_FOLDERS); 1231 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", 1232 ACCOUNT_PICK_TRASH_FOLDER); 1233 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", 1234 ACCOUNT_PICK_SENT_FOLDER); 1235 sURIMatcher.addURI(EmailContent.AUTHORITY, "uipurgefolder/#", UI_PURGE_FOLDER); 1236 } 1237 } 1238 1239 /** 1240 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1241 * always be in sync (i.e. there are two database or NO databases). This code will delete 1242 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1243 * will exist after either of the individual databases is deleted due to data corruption. 1244 */ 1245 public void checkDatabases() { 1246 synchronized (sDatabaseLock) { 1247 // Uncache the databases 1248 if (mDatabase != null) { 1249 mDatabase = null; 1250 } 1251 if (mBodyDatabase != null) { 1252 mBodyDatabase = null; 1253 } 1254 // Look for orphans, and delete as necessary; these must always be in sync 1255 final File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1256 final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1257 1258 // TODO Make sure attachments are deleted 1259 if (databaseFile.exists() && !bodyFile.exists()) { 1260 LogUtils.w(TAG, "Deleting orphaned EmailProvider database..."); 1261 getContext().deleteDatabase(DATABASE_NAME); 1262 } else if (bodyFile.exists() && !databaseFile.exists()) { 1263 LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1264 getContext().deleteDatabase(BODY_DATABASE_NAME); 1265 } 1266 } 1267 } 1268 1269 @Override 1270 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1271 String sortOrder) { 1272 Cursor c = null; 1273 int match; 1274 try { 1275 match = findMatch(uri, "query"); 1276 } catch (IllegalArgumentException e) { 1277 String uriString = uri.toString(); 1278 // If we were passed an illegal uri, see if it ends in /-1 1279 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1280 if (uriString != null && uriString.endsWith("/-1")) { 1281 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1282 match = findMatch(uri, "query"); 1283 switch (match) { 1284 case BODY_ID: 1285 case MESSAGE_ID: 1286 case DELETED_MESSAGE_ID: 1287 case UPDATED_MESSAGE_ID: 1288 case ATTACHMENT_ID: 1289 case MAILBOX_ID: 1290 case ACCOUNT_ID: 1291 case HOSTAUTH_ID: 1292 case CREDENTIAL_ID: 1293 case POLICY_ID: 1294 return new MatrixCursorWithCachedColumns(projection, 0); 1295 } 1296 } 1297 throw e; 1298 } 1299 Context context = getContext(); 1300 // See the comment at delete(), above 1301 SQLiteDatabase db = getDatabase(context); 1302 int table = match >> BASE_SHIFT; 1303 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1304 String id; 1305 1306 String tableName = TABLE_NAMES.valueAt(table); 1307 1308 try { 1309 switch (match) { 1310 // First, dispatch queries from UnifiedEmail 1311 case UI_SEARCH: 1312 c = uiSearch(uri, projection); 1313 return c; 1314 case UI_ACCTS: 1315 final String suppressParam = 1316 uri.getQueryParameter(EmailContent.SUPPRESS_COMBINED_ACCOUNT_PARAM); 1317 final boolean suppressCombined = 1318 suppressParam != null && Boolean.parseBoolean(suppressParam); 1319 c = uiAccounts(projection, suppressCombined); 1320 return c; 1321 case UI_UNDO: 1322 return uiUndo(projection); 1323 case UI_SUBFOLDERS: 1324 case UI_MESSAGES: 1325 case UI_MESSAGE: 1326 case UI_FOLDER: 1327 case UI_INBOX: 1328 case UI_ACCOUNT: 1329 case UI_ATTACHMENT: 1330 case UI_ATTACHMENTS: 1331 case UI_ATTACHMENT_BY_CID: 1332 case UI_CONVERSATION: 1333 case UI_RECENT_FOLDERS: 1334 case UI_FULL_FOLDERS: 1335 case UI_ALL_FOLDERS: 1336 // For now, we don't allow selection criteria within these queries 1337 if (selection != null || selectionArgs != null) { 1338 throw new IllegalArgumentException("UI queries can't have selection/args"); 1339 } 1340 1341 final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER); 1342 final boolean unseenOnly = 1343 seenParam != null && Boolean.FALSE.toString().equals(seenParam); 1344 1345 c = uiQuery(match, uri, projection, unseenOnly); 1346 return c; 1347 case UI_FOLDERS: 1348 c = uiFolders(uri, projection); 1349 return c; 1350 case UI_FOLDER_LOAD_MORE: 1351 c = uiFolderLoadMore(getMailbox(uri)); 1352 return c; 1353 case UI_FOLDER_REFRESH: 1354 c = uiFolderRefresh(getMailbox(uri), 0); 1355 return c; 1356 case MAILBOX_NOTIFICATION: 1357 c = notificationQuery(uri); 1358 return c; 1359 case MAILBOX_MOST_RECENT_MESSAGE: 1360 c = mostRecentMessageQuery(uri); 1361 return c; 1362 case MAILBOX_MESSAGE_COUNT: 1363 c = getMailboxMessageCount(uri); 1364 return c; 1365 case MESSAGE_MOVE: 1366 return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs, 1367 null, null, sortOrder, limit); 1368 case MESSAGE_STATE_CHANGE: 1369 return db.query(MessageStateChange.TABLE_NAME, projection, selection, 1370 selectionArgs, null, null, sortOrder, limit); 1371 case MESSAGE: 1372 case UPDATED_MESSAGE: 1373 case DELETED_MESSAGE: 1374 case ATTACHMENT: 1375 case MAILBOX: 1376 case ACCOUNT: 1377 case HOSTAUTH: 1378 case CREDENTIAL: 1379 case POLICY: 1380 c = db.query(tableName, projection, 1381 selection, selectionArgs, null, null, sortOrder, limit); 1382 break; 1383 case QUICK_RESPONSE: 1384 c = uiQuickResponse(projection); 1385 break; 1386 case BODY: 1387 case BODY_ID: { 1388 final ProjectionMap map = new ProjectionMap.Builder() 1389 .addAll(projection) 1390 .build(); 1391 if (map.containsKey(BodyColumns.HTML_CONTENT) || 1392 map.containsKey(BodyColumns.TEXT_CONTENT)) { 1393 throw new IllegalArgumentException( 1394 "Body content cannot be returned in the cursor"); 1395 } 1396 1397 final ContentValues cv = new ContentValues(2); 1398 cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml", 1399 BodyColumns.MESSAGE_KEY)); 1400 cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText", 1401 BodyColumns.MESSAGE_KEY)); 1402 1403 final StringBuilder sb = genSelect(map, projection, cv); 1404 sb.append(" FROM ").append(Body.TABLE_NAME); 1405 if (match == BODY_ID) { 1406 id = uri.getPathSegments().get(1); 1407 sb.append(" WHERE ").append(whereWithId(id, selection)); 1408 } else if (!TextUtils.isEmpty(selection)) { 1409 sb.append(" WHERE ").append(selection); 1410 } 1411 if (!TextUtils.isEmpty(sortOrder)) { 1412 sb.append(" ORDER BY ").append(sortOrder); 1413 } 1414 if (!TextUtils.isEmpty(limit)) { 1415 sb.append(" LIMIT ").append(limit); 1416 } 1417 c = db.rawQuery(sb.toString(), selectionArgs); 1418 break; 1419 } 1420 case MESSAGE_ID: 1421 case DELETED_MESSAGE_ID: 1422 case UPDATED_MESSAGE_ID: 1423 case ATTACHMENT_ID: 1424 case MAILBOX_ID: 1425 case HOSTAUTH_ID: 1426 case CREDENTIAL_ID: 1427 case POLICY_ID: 1428 id = uri.getPathSegments().get(1); 1429 c = db.query(tableName, projection, whereWithId(id, selection), 1430 selectionArgs, null, null, sortOrder, limit); 1431 break; 1432 case ACCOUNT_ID: 1433 id = uri.getPathSegments().get(1); 1434 // There seems to be an issue with smart forwarding sometimes including the 1435 // quoted text from the wrong message. For now, we just disable it. 1436 final String[] alternateProjection = new String[projection.length]; 1437 for (int i = 0; i < projection.length; i++) { 1438 String column = projection[i]; 1439 if (TextUtils.equals(column, AccountColumns.FLAGS)) { 1440 alternateProjection[i] = AccountColumns.FLAGS + " & ~" + 1441 Account.FLAGS_SUPPORTS_SMART_FORWARD + " AS " + 1442 AccountColumns.FLAGS; 1443 } else { 1444 alternateProjection[i] = projection[i]; 1445 } 1446 } 1447 1448 c = db.query(tableName, alternateProjection, whereWithId(id, selection), 1449 selectionArgs, null, null, sortOrder, limit); 1450 break; 1451 case QUICK_RESPONSE_ID: 1452 id = uri.getPathSegments().get(1); 1453 c = uiQuickResponseId(projection, id); 1454 break; 1455 case ATTACHMENTS_MESSAGE_ID: 1456 // All attachments for the given message 1457 id = uri.getPathSegments().get(2); 1458 c = db.query(Attachment.TABLE_NAME, projection, 1459 whereWith(AttachmentColumns.MESSAGE_KEY + "=" + id, selection), 1460 selectionArgs, null, null, sortOrder, limit); 1461 break; 1462 case QUICK_RESPONSE_ACCOUNT_ID: 1463 // All quick responses for the given account 1464 id = uri.getPathSegments().get(2); 1465 c = uiQuickResponseAccount(projection, id); 1466 break; 1467 case ATTACHMENTS_CACHED_FILE_ACCESS: 1468 if (projection == null) { 1469 projection = 1470 new String[] { 1471 AttachmentUtilities.Columns._ID, 1472 AttachmentUtilities.Columns.DATA, 1473 }; 1474 } 1475 // Map the columns of our attachment table to the columns defined in 1476 // AttachmentUtils. These are a superset of OpenableColumns. 1477 // This mirrors similar code in AttachmentProvider. 1478 c = db.query(Attachment.TABLE_NAME, 1479 CACHED_FILE_QUERY_PROJECTION, AttachmentColumns.CACHED_FILE + "=?", 1480 new String[]{uri.toString()}, null, null, null, null); 1481 try { 1482 if (c.getCount() > 1) { 1483 LogUtils.e(TAG, "multiple results querying CACHED_FILE_ACCESS %s", uri); 1484 } 1485 if (c != null && c.moveToFirst()) { 1486 MatrixCursor ret = new MatrixCursorWithCachedColumns(projection); 1487 Object[] values = new Object[projection.length]; 1488 for (int i = 0, count = projection.length; i < count; i++) { 1489 String column = projection[i]; 1490 if (AttachmentUtilities.Columns._ID.equals(column)) { 1491 values[i] = c.getLong( 1492 c.getColumnIndexOrThrow(AttachmentColumns._ID)); 1493 } 1494 else if (AttachmentUtilities.Columns.DATA.equals(column)) { 1495 values[i] = c.getString( 1496 c.getColumnIndexOrThrow(AttachmentColumns.CONTENT_URI)); 1497 } 1498 else if (AttachmentUtilities.Columns.DISPLAY_NAME.equals(column)) { 1499 values[i] = c.getString( 1500 c.getColumnIndexOrThrow(AttachmentColumns.FILENAME)); 1501 } 1502 else if (AttachmentUtilities.Columns.SIZE.equals(column)) { 1503 values[i] = c.getInt( 1504 c.getColumnIndexOrThrow(AttachmentColumns.SIZE)); 1505 } else { 1506 LogUtils.e(TAG, 1507 "unexpected column %s requested for CACHED_FILE", 1508 column); 1509 } 1510 } 1511 ret.addRow(values); 1512 return ret; 1513 } 1514 } finally { 1515 if (c != null) { 1516 c.close(); 1517 } 1518 } 1519 return null; 1520 default: 1521 throw new IllegalArgumentException("Unknown URI " + uri); 1522 } 1523 } catch (SQLiteException e) { 1524 checkDatabases(); 1525 throw e; 1526 } catch (RuntimeException e) { 1527 checkDatabases(); 1528 e.printStackTrace(); 1529 throw e; 1530 } finally { 1531 if (c == null) { 1532 // This should never happen, but let's be sure to log it... 1533 // TODO: There are actually cases where c == null is expected, for example 1534 // UI_FOLDER_LOAD_MORE. 1535 // Demoting this to a warning for now until we figure out what to do with it. 1536 LogUtils.w(TAG, "Query returning null for uri: %s selection: %s", uri, selection); 1537 } 1538 } 1539 1540 if ((c != null) && !isTemporary()) { 1541 c.setNotificationUri(getContext().getContentResolver(), uri); 1542 } 1543 return c; 1544 } 1545 1546 private static String whereWithId(String id, String selection) { 1547 StringBuilder sb = new StringBuilder(256); 1548 sb.append("_id="); 1549 sb.append(id); 1550 if (selection != null) { 1551 sb.append(" AND ("); 1552 sb.append(selection); 1553 sb.append(')'); 1554 } 1555 return sb.toString(); 1556 } 1557 1558 /** 1559 * Combine a locally-generated selection with a user-provided selection 1560 * 1561 * This introduces risk that the local selection might insert incorrect chars 1562 * into the SQL, so use caution. 1563 * 1564 * @param where locally-generated selection, must not be null 1565 * @param selection user-provided selection, may be null 1566 * @return a single selection string 1567 */ 1568 private static String whereWith(String where, String selection) { 1569 if (selection == null) { 1570 return where; 1571 } 1572 return where + " AND (" + selection + ")"; 1573 } 1574 1575 /** 1576 * Restore a HostAuth from a database, given its unique id 1577 * @param db the database 1578 * @param id the unique id (_id) of the row 1579 * @return a fully populated HostAuth or null if the row does not exist 1580 */ 1581 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1582 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1583 HostAuthColumns._ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1584 try { 1585 if (c.moveToFirst()) { 1586 HostAuth hostAuth = new HostAuth(); 1587 hostAuth.restore(c); 1588 return hostAuth; 1589 } 1590 return null; 1591 } finally { 1592 c.close(); 1593 } 1594 } 1595 1596 /** 1597 * Copy the Account and HostAuth tables from one database to another 1598 * @param fromDatabase the source database 1599 * @param toDatabase the destination database 1600 * @return the number of accounts copied, or -1 if an error occurred 1601 */ 1602 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1603 if (fromDatabase == null || toDatabase == null) return -1; 1604 1605 // Lock both databases; for the "from" database, we don't want anyone changing it from 1606 // under us; for the "to" database, we want to make the operation atomic 1607 int copyCount = 0; 1608 fromDatabase.beginTransaction(); 1609 try { 1610 toDatabase.beginTransaction(); 1611 try { 1612 // Delete anything hanging around here 1613 toDatabase.delete(Account.TABLE_NAME, null, null); 1614 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1615 1616 // Get our account cursor 1617 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1618 null, null, null, null, null); 1619 if (c == null) return 0; 1620 LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount()); 1621 try { 1622 // Loop through accounts, copying them and associated host auth's 1623 while (c.moveToNext()) { 1624 Account account = new Account(); 1625 account.restore(c); 1626 1627 // Clear security sync key and sync key, as these were specific to the 1628 // state of the account, and we've reset that... 1629 // Clear policy key so that we can re-establish policies from the server 1630 // TODO This is pretty EAS specific, but there's a lot of that around 1631 account.mSecuritySyncKey = null; 1632 account.mSyncKey = null; 1633 account.mPolicyKey = 0; 1634 1635 // Copy host auth's and update foreign keys 1636 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1637 account.mHostAuthKeyRecv); 1638 1639 // The account might have gone away, though very unlikely 1640 if (hostAuth == null) continue; 1641 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1642 hostAuth.toContentValues()); 1643 1644 // EAS accounts have no send HostAuth 1645 if (account.mHostAuthKeySend > 0) { 1646 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1647 // Belt and suspenders; I can't imagine that this is possible, 1648 // since we checked the validity of the account above, and the 1649 // database is now locked 1650 if (hostAuth == null) continue; 1651 account.mHostAuthKeySend = toDatabase.insert( 1652 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1653 } 1654 1655 // Now, create the account in the "to" database 1656 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1657 copyCount++; 1658 } 1659 } finally { 1660 c.close(); 1661 } 1662 1663 // Say it's ok to commit 1664 toDatabase.setTransactionSuccessful(); 1665 } finally { 1666 toDatabase.endTransaction(); 1667 } 1668 } catch (SQLiteException ex) { 1669 LogUtils.w(TAG, "Exception while copying account tables", ex); 1670 copyCount = -1; 1671 } finally { 1672 fromDatabase.endTransaction(); 1673 } 1674 return copyCount; 1675 } 1676 1677 /** 1678 * Backup account data, returning the number of accounts backed up 1679 */ 1680 private static int backupAccounts(final Context context, final SQLiteDatabase db) { 1681 final AccountManager am = AccountManager.get(context); 1682 final Cursor accountCursor = db.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1683 null, null, null, null, null); 1684 int updatedCount = 0; 1685 try { 1686 while (accountCursor.moveToNext()) { 1687 final Account account = new Account(); 1688 account.restore(accountCursor); 1689 EmailServiceInfo serviceInfo = 1690 EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); 1691 if (serviceInfo == null) { 1692 LogUtils.d(LogUtils.TAG, "Could not find service info for account"); 1693 continue; 1694 } 1695 final String jsonString = account.toJsonString(context); 1696 final android.accounts.Account amAccount = 1697 account.getAccountManagerAccount(serviceInfo.accountType); 1698 am.setUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG, jsonString); 1699 updatedCount++; 1700 } 1701 } finally { 1702 accountCursor.close(); 1703 } 1704 return updatedCount; 1705 } 1706 1707 /** 1708 * Restore account data, returning the number of accounts restored 1709 */ 1710 private static int restoreAccounts(final Context context) { 1711 final Collection<EmailServiceInfo> infos = EmailServiceUtils.getServiceInfoList(context); 1712 // Find all possible account types 1713 final Set<String> accountTypes = new HashSet<String>(3); 1714 for (final EmailServiceInfo info : infos) { 1715 if (!TextUtils.isEmpty(info.accountType)) { 1716 // accountType will be empty for the gmail stub entry 1717 accountTypes.add(info.accountType); 1718 } 1719 } 1720 // Find all accounts we own 1721 final List<android.accounts.Account> amAccounts = new ArrayList<android.accounts.Account>(); 1722 final AccountManager am = AccountManager.get(context); 1723 for (final String accountType : accountTypes) { 1724 amAccounts.addAll(Arrays.asList(am.getAccountsByType(accountType))); 1725 } 1726 // Try to restore them from saved JSON 1727 int restoredCount = 0; 1728 for (final android.accounts.Account amAccount : amAccounts) { 1729 final String jsonString = am.getUserData(amAccount, ACCOUNT_MANAGER_JSON_TAG); 1730 if (TextUtils.isEmpty(jsonString)) { 1731 continue; 1732 } 1733 final Account account = Account.fromJsonString(jsonString); 1734 if (account != null) { 1735 AccountSettingsUtils.commitSettings(context, account); 1736 final Bundle extras = new Bundle(3); 1737 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 1738 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 1739 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 1740 ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); 1741 restoredCount++; 1742 } 1743 } 1744 return restoredCount; 1745 } 1746 1747 private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s (" 1748 + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + "," 1749 + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ","; 1750 1751 private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, " 1752 + "(select " + MessageColumns.SERVER_ID + " from " + 1753 Message.TABLE_NAME + " where _id=%s)," 1754 + "(select " + MessageColumns.ACCOUNT_KEY + " from " + 1755 Message.TABLE_NAME + " where _id=%s)," 1756 + MessageMove.STATUS_NONE_STRING + ","; 1757 1758 /** 1759 * Formatting string to generate the SQL statement for inserting into MessageMove. 1760 * The formatting parameters are: 1761 * table name, message id x 4, destination folder id, message id, destination folder id. 1762 * Duplications are needed for sub-selects. 1763 */ 1764 private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1765 + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + "," 1766 + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID 1767 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1768 + "(select " + MessageColumns.MAILBOX_KEY + 1769 " from " + Message.TABLE_NAME + " where _id=%s)," + "%d," 1770 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select " 1771 + MessageColumns.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s))," 1772 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))"; 1773 1774 /** 1775 * Insert a row into the MessageMove table when that message is moved. 1776 * @param db The {@link SQLiteDatabase}. 1777 * @param messageId The id of the message being moved. 1778 * @param dstFolderKey The folder to which the message is being moved. 1779 */ 1780 private void addToMessageMove(final SQLiteDatabase db, final String messageId, 1781 final long dstFolderKey) { 1782 db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME, 1783 messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey)); 1784 } 1785 1786 /** 1787 * Formatting string to generate the SQL statement for inserting into MessageStateChange. 1788 * The formatting parameters are: 1789 * table name, message id x 4, new flag read, message id, new flag favorite. 1790 * Duplications are needed for sub-selects. 1791 */ 1792 private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1793 + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + "," 1794 + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE 1795 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1796 + "(select " + MessageColumns.FLAG_READ + 1797 " from " + Message.TABLE_NAME + " where _id=%s)," + "%d," 1798 + "(select " + MessageColumns.FLAG_FAVORITE + 1799 " from " + Message.TABLE_NAME + " where _id=%s)," + "%d)"; 1800 1801 private void addToMessageStateChange(final SQLiteDatabase db, final String messageId, 1802 final int newFlagRead, final int newFlagFavorite) { 1803 db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT, 1804 MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId, 1805 newFlagRead, messageId, newFlagFavorite)); 1806 } 1807 1808 // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? 1809 // group by serverId) where dupes > 1; 1810 private static final String ACCOUNT_INTEGRITY_SQL = 1811 "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + 1812 " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; 1813 1814 1815 // Query to get the protocol for a message. Temporary to switch between new and old upsync 1816 // behavior; should go away when IMAP gets converted. 1817 private static final String GET_MESSAGE_DETAILS = "SELECT" 1818 + " h." + HostAuthColumns.PROTOCOL + "," 1819 + " m." + MessageColumns.MAILBOX_KEY + "," 1820 + " a." + AccountColumns._ID 1821 + " FROM " + Message.TABLE_NAME + " AS m" 1822 + " INNER JOIN " + Account.TABLE_NAME + " AS a" 1823 + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns._ID 1824 + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" 1825 + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID 1826 + " WHERE m." + MessageColumns._ID + "=?"; 1827 private static final int INDEX_PROTOCOL = 0; 1828 private static final int INDEX_MAILBOX_KEY = 1; 1829 private static final int INDEX_ACCOUNT_KEY = 2; 1830 1831 /** 1832 * Query to get the protocol and email address for an account. Note that this uses 1833 * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns. 1834 */ 1835 private static final String GET_ACCOUNT_DETAILS = "SELECT" 1836 + " h." + HostAuthColumns.PROTOCOL + "," 1837 + " a." + AccountColumns.EMAIL_ADDRESS + "," 1838 + " a." + AccountColumns.SYNC_KEY 1839 + " FROM " + Account.TABLE_NAME + " AS a" 1840 + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" 1841 + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns._ID 1842 + " WHERE a." + AccountColumns._ID + "=?"; 1843 private static final int INDEX_EMAIL_ADDRESS = 1; 1844 private static final int INDEX_SYNC_KEY = 2; 1845 1846 /** 1847 * Restart push if we need it (currently only for Exchange accounts). 1848 * @param context A {@link Context}. 1849 * @param db The {@link SQLiteDatabase}. 1850 * @param id The id of the thing we're looking for. 1851 * @return Whether or not we sent a request to restart the push. 1852 */ 1853 private static boolean restartPush(final Context context, final SQLiteDatabase db, 1854 final String id) { 1855 final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id}); 1856 if (c != null) { 1857 try { 1858 if (c.moveToFirst()) { 1859 final String protocol = c.getString(INDEX_PROTOCOL); 1860 // Only restart push for EAS accounts that have completed initial sync. 1861 if (context.getString(R.string.protocol_eas).equals(protocol) && 1862 !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) { 1863 final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS); 1864 final android.accounts.Account account = 1865 getAccountManagerAccount(context, emailAddress, protocol); 1866 if (account != null) { 1867 restartPush(account); 1868 return true; 1869 } 1870 } 1871 } 1872 } finally { 1873 c.close(); 1874 } 1875 } 1876 return false; 1877 } 1878 1879 /** 1880 * Restart push if a mailbox's settings change in a way that requires it. 1881 * @param context A {@link Context}. 1882 * @param db The {@link SQLiteDatabase}. 1883 * @param values The {@link ContentValues} that were updated for the mailbox. 1884 * @param accountId The id of the account for this mailbox. 1885 * @return Whether or not the push was restarted. 1886 */ 1887 private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db, 1888 final ContentValues values, final String accountId) { 1889 if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) || 1890 values.containsKey(MailboxColumns.SYNC_INTERVAL)) { 1891 return restartPush(context, db, accountId); 1892 } 1893 return false; 1894 } 1895 1896 /** 1897 * Restart push if an account's settings change in a way that requires it. 1898 * @param context A {@link Context}. 1899 * @param db The {@link SQLiteDatabase}. 1900 * @param values The {@link ContentValues} that were updated for the account. 1901 * @param accountId The id of the account. 1902 * @return Whether or not the push was restarted. 1903 */ 1904 private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db, 1905 final ContentValues values, final String accountId) { 1906 if (values.containsKey(AccountColumns.SYNC_LOOKBACK) || 1907 values.containsKey(AccountColumns.SYNC_INTERVAL)) { 1908 return restartPush(context, db, accountId); 1909 } 1910 return false; 1911 } 1912 1913 @Override 1914 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1915 LogUtils.d(TAG, "Update: " + uri); 1916 // Handle this special case the fastest possible way 1917 if (INTEGRITY_CHECK_URI.equals(uri)) { 1918 checkDatabases(); 1919 return 0; 1920 } else if (ACCOUNT_BACKUP_URI.equals(uri)) { 1921 return backupAccounts(getContext(), getDatabase(getContext())); 1922 } 1923 1924 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1925 Uri notificationUri = EmailContent.CONTENT_URI; 1926 1927 final int match = findMatch(uri, "update"); 1928 final Context context = getContext(); 1929 // See the comment at delete(), above 1930 final SQLiteDatabase db = getDatabase(context); 1931 final int table = match >> BASE_SHIFT; 1932 int result; 1933 1934 // We do NOT allow setting of unreadCount/messageCount via the provider 1935 // These columns are maintained via triggers 1936 if (match == MAILBOX_ID || match == MAILBOX) { 1937 values.remove(MailboxColumns.UNREAD_COUNT); 1938 values.remove(MailboxColumns.MESSAGE_COUNT); 1939 } 1940 1941 final String tableName = TABLE_NAMES.valueAt(table); 1942 String id = "0"; 1943 1944 try { 1945 switch (match) { 1946 case ACCOUNT_PICK_TRASH_FOLDER: 1947 return pickTrashFolder(uri); 1948 case ACCOUNT_PICK_SENT_FOLDER: 1949 return pickSentFolder(uri); 1950 case UI_ACCTSETTINGS: 1951 return uiUpdateSettings(context, values); 1952 case UI_FOLDER: 1953 return uiUpdateFolder(context, uri, values); 1954 case UI_RECENT_FOLDERS: 1955 return uiUpdateRecentFolders(uri, values); 1956 case UI_DEFAULT_RECENT_FOLDERS: 1957 return uiPopulateRecentFolders(uri); 1958 case UI_ATTACHMENT: 1959 return uiUpdateAttachment(uri, values); 1960 case UI_MESSAGE: 1961 return uiUpdateMessage(uri, values); 1962 case ACCOUNT_CHECK: 1963 id = uri.getLastPathSegment(); 1964 // With any error, return 1 (a failure) 1965 int res = 1; 1966 Cursor ic = null; 1967 try { 1968 ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); 1969 if (ic.moveToFirst()) { 1970 res = ic.getInt(0); 1971 } 1972 } finally { 1973 if (ic != null) { 1974 ic.close(); 1975 } 1976 } 1977 // Count of duplicated mailboxes 1978 return res; 1979 case MESSAGE_SELECTION: 1980 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 1981 selectionArgs, null, null, null); 1982 try { 1983 if (findCursor.moveToFirst()) { 1984 return update(ContentUris.withAppendedId( 1985 Message.CONTENT_URI, 1986 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 1987 values, null, null); 1988 } else { 1989 return 0; 1990 } 1991 } finally { 1992 findCursor.close(); 1993 } 1994 case SYNCED_MESSAGE_ID: 1995 case UPDATED_MESSAGE_ID: 1996 case MESSAGE_ID: 1997 case ATTACHMENT_ID: 1998 case MAILBOX_ID: 1999 case ACCOUNT_ID: 2000 case HOSTAUTH_ID: 2001 case CREDENTIAL_ID: 2002 case QUICK_RESPONSE_ID: 2003 case POLICY_ID: 2004 id = uri.getPathSegments().get(1); 2005 if (match == SYNCED_MESSAGE_ID) { 2006 // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well. 2007 boolean isEas = false; 2008 long mailboxId = -1; 2009 long accountId = -1; 2010 final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id}); 2011 if (c != null) { 2012 try { 2013 if (c.moveToFirst()) { 2014 final String protocol = c.getString(INDEX_PROTOCOL); 2015 isEas = context.getString(R.string.protocol_eas) 2016 .equals(protocol); 2017 mailboxId = c.getLong(INDEX_MAILBOX_KEY); 2018 accountId = c.getLong(INDEX_ACCOUNT_KEY); 2019 } 2020 } finally { 2021 c.close(); 2022 } 2023 } 2024 2025 if (isEas) { 2026 // EAS uses the new upsync classes. 2027 Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY); 2028 if (dstFolderId != null) { 2029 addToMessageMove(db, id, dstFolderId); 2030 } 2031 Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ); 2032 Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE); 2033 int flagReadValue = (flagRead != null) ? 2034 flagRead : MessageStateChange.VALUE_UNCHANGED; 2035 int flagFavoriteValue = (flagFavorite != null) ? 2036 flagFavorite : MessageStateChange.VALUE_UNCHANGED; 2037 if (flagRead != null || flagFavorite != null) { 2038 addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue); 2039 } 2040 2041 // Request a sync for the messages mailbox so the update will upsync. 2042 // This is normally done with ContentResolver.notifyUpdate() but doesn't 2043 // work for Exchange because the Sync Adapter is declared as 2044 // android:supportsUploading="false". Changing it to true is not trivial 2045 // because that would require us to protect all calls to notifyUpdate() 2046 // with syncToServer=false except in cases where we actually want to 2047 // upsync. 2048 // TODO: Look into making Exchange Sync Adapter supportsUploading=true 2049 // Since we can't use the Sync Manager "delayed-sync" feature which 2050 // applies only to UPLOAD syncs, we need to do this ourselves. The 2051 // purpose of this is not to spam syncs when making frequent 2052 // modifications. 2053 final Handler handler = getDelayedSyncHandler(); 2054 final android.accounts.Account amAccount = 2055 getAccountManagerAccount(accountId); 2056 if (amAccount != null) { 2057 final SyncRequestMessage request = new SyncRequestMessage( 2058 uri.getAuthority(), amAccount, mailboxId); 2059 synchronized (mDelayedSyncRequests) { 2060 if (!mDelayedSyncRequests.contains(request)) { 2061 mDelayedSyncRequests.add(request); 2062 final android.os.Message message = 2063 handler.obtainMessage(0, request); 2064 handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS); 2065 } 2066 } 2067 } else { 2068 LogUtils.d(TAG, 2069 "Attempted to start delayed sync for invalid account %d", 2070 accountId); 2071 } 2072 } else { 2073 // Old way of doing upsync. 2074 // For synced messages, first copy the old message to the updated table 2075 // Note the insert or ignore semantics, guaranteeing that only the first 2076 // update will be reflected in the updated message table; therefore this 2077 // row will always have the "original" data 2078 db.execSQL(UPDATED_MESSAGE_INSERT + id); 2079 } 2080 } else if (match == MESSAGE_ID) { 2081 db.execSQL(UPDATED_MESSAGE_DELETE + id); 2082 } 2083 result = db.update(tableName, values, whereWithId(id, selection), 2084 selectionArgs); 2085 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 2086 handleMessageUpdateNotifications(uri, id, values); 2087 } else if (match == ATTACHMENT_ID) { 2088 long attId = Integer.parseInt(id); 2089 if (values.containsKey(AttachmentColumns.FLAGS)) { 2090 int flags = values.getAsInteger(AttachmentColumns.FLAGS); 2091 mAttachmentService.attachmentChanged(context, attId, flags); 2092 } 2093 // Notify UI if necessary; there are only two columns we can change that 2094 // would be worth a notification 2095 if (values.containsKey(AttachmentColumns.UI_STATE) || 2096 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { 2097 // Notify on individual attachment 2098 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 2099 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 2100 if (att != null) { 2101 // And on owning Message 2102 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); 2103 } 2104 } 2105 } else if (match == MAILBOX_ID) { 2106 final long accountId = Mailbox.getAccountIdForMailbox(context, id); 2107 notifyUIFolder(id, accountId); 2108 restartPushForMailbox(context, db, values, Long.toString(accountId)); 2109 } else if (match == ACCOUNT_ID) { 2110 updateAccountSyncInterval(Long.parseLong(id), values); 2111 // Notify individual account and "all accounts" 2112 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 2113 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 2114 restartPushForAccount(context, db, values, id); 2115 } 2116 break; 2117 case BODY_ID: { 2118 final ContentValues updateValues = new ContentValues(values); 2119 updateValues.remove(BodyColumns.HTML_CONTENT); 2120 updateValues.remove(BodyColumns.TEXT_CONTENT); 2121 2122 result = db.update(tableName, updateValues, whereWithId(id, selection), 2123 selectionArgs); 2124 2125 if (values.containsKey(BodyColumns.HTML_CONTENT) || 2126 values.containsKey(BodyColumns.TEXT_CONTENT)) { 2127 final long messageId; 2128 if (values.containsKey(BodyColumns.MESSAGE_KEY)) { 2129 messageId = values.getAsLong(BodyColumns.MESSAGE_KEY); 2130 } else { 2131 final long bodyId = Long.parseLong(id); 2132 final SQLiteStatement sql = db.compileStatement( 2133 "select " + BodyColumns.MESSAGE_KEY + 2134 " from " + Body.TABLE_NAME + 2135 " where " + BodyColumns._ID + "=" + Long 2136 .toString(bodyId) 2137 ); 2138 messageId = sql.simpleQueryForLong(); 2139 } 2140 writeBodyFiles(context, messageId, values); 2141 } 2142 break; 2143 } 2144 case BODY: { 2145 final ContentValues updateValues = new ContentValues(values); 2146 updateValues.remove(BodyColumns.HTML_CONTENT); 2147 updateValues.remove(BodyColumns.TEXT_CONTENT); 2148 2149 result = db.update(tableName, updateValues, selection, selectionArgs); 2150 2151 if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) { 2152 // TODO: This is a hack. Notably, the selection equality test above 2153 // is hokey at best. 2154 LogUtils.i(TAG, "Body Update to non-existent row, morphing to insert"); 2155 final ContentValues insertValues = new ContentValues(values); 2156 insertValues.put(BodyColumns.MESSAGE_KEY, selectionArgs[0]); 2157 insert(Body.CONTENT_URI, insertValues); 2158 } else { 2159 // possibly need to write new body values 2160 if (values.containsKey(BodyColumns.HTML_CONTENT) || 2161 values.containsKey(BodyColumns.TEXT_CONTENT)) { 2162 final long messageIds[]; 2163 if (values.containsKey(BodyColumns.MESSAGE_KEY)) { 2164 messageIds = new long[] {values.getAsLong(BodyColumns.MESSAGE_KEY)}; 2165 } else if (values.containsKey(BodyColumns._ID)) { 2166 final long bodyId = values.getAsLong(BodyColumns._ID); 2167 final SQLiteStatement sql = db.compileStatement( 2168 "select " + BodyColumns.MESSAGE_KEY + 2169 " from " + Body.TABLE_NAME + 2170 " where " + BodyColumns._ID + "=" + Long 2171 .toString(bodyId) 2172 ); 2173 messageIds = new long[] {sql.simpleQueryForLong()}; 2174 } else { 2175 final String proj[] = {BodyColumns.MESSAGE_KEY}; 2176 final Cursor c = db.query(Body.TABLE_NAME, proj, 2177 selection, selectionArgs, 2178 null, null, null); 2179 try { 2180 final int count = c.getCount(); 2181 if (count == 0) { 2182 throw new IllegalStateException("Can't find body record"); 2183 } 2184 messageIds = new long[count]; 2185 int i = 0; 2186 while (c.moveToNext()) { 2187 messageIds[i++] = c.getLong(0); 2188 } 2189 } finally { 2190 c.close(); 2191 } 2192 } 2193 // This is probably overkill 2194 for (int i = 0; i < messageIds.length; i++) { 2195 final long messageId = messageIds[i]; 2196 writeBodyFiles(context, messageId, values); 2197 } 2198 } 2199 } 2200 break; 2201 } 2202 case MESSAGE: 2203 decodeEmailAddresses(values); 2204 case UPDATED_MESSAGE: 2205 case ATTACHMENT: 2206 case MAILBOX: 2207 case ACCOUNT: 2208 case HOSTAUTH: 2209 case CREDENTIAL: 2210 case POLICY: 2211 if (match == ATTACHMENT) { 2212 if (values.containsKey(AttachmentColumns.LOCATION) && 2213 TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { 2214 LogUtils.w(TAG, new Throwable(), "attachment with blank location"); 2215 } 2216 } 2217 result = db.update(tableName, values, selection, selectionArgs); 2218 break; 2219 case MESSAGE_MOVE: 2220 result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs); 2221 break; 2222 case MESSAGE_STATE_CHANGE: 2223 result = db.update(MessageStateChange.TABLE_NAME, values, selection, 2224 selectionArgs); 2225 break; 2226 default: 2227 throw new IllegalArgumentException("Unknown URI " + uri); 2228 } 2229 } catch (SQLiteException e) { 2230 checkDatabases(); 2231 throw e; 2232 } 2233 2234 // Notify all notifier cursors if some records where changed in the database 2235 if (result > 0) { 2236 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 2237 notifyUI(notificationUri, null); 2238 } 2239 return result; 2240 } 2241 2242 private void updateSyncStatus(final Bundle extras) { 2243 final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); 2244 final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE); 2245 final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id); 2246 notifyUI(uri, null); 2247 final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS; 2248 if (inProgress) { 2249 RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id); 2250 } else { 2251 final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT); 2252 final ContentValues values = new ContentValues(); 2253 values.put(Mailbox.UI_LAST_SYNC_RESULT, result); 2254 mDatabase.update( 2255 Mailbox.TABLE_NAME, 2256 values, 2257 WHERE_ID, 2258 new String[] { String.valueOf(id) }); 2259 } 2260 } 2261 2262 @Override 2263 public Bundle call(String method, String arg, Bundle extras) { 2264 LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg); 2265 2266 // Handle queries for the device friendly name. 2267 // TODO: This should eventually be a device property, not defined by the app. 2268 if (TextUtils.equals(method, EmailContent.DEVICE_FRIENDLY_NAME)) { 2269 final Bundle bundle = new Bundle(1); 2270 // TODO: For now, just use the model name since we don't yet have a user-supplied name. 2271 bundle.putString(EmailContent.DEVICE_FRIENDLY_NAME, Build.MODEL); 2272 return bundle; 2273 } 2274 2275 // Handle sync status callbacks. 2276 if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) { 2277 updateSyncStatus(extras); 2278 return null; 2279 } 2280 if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) { 2281 fixParentKeys(getDatabase(getContext())); 2282 return null; 2283 } 2284 2285 // Handle send & save. 2286 final Uri accountUri = Uri.parse(arg); 2287 final long accountId = Long.parseLong(accountUri.getPathSegments().get(1)); 2288 2289 Uri messageUri = null; 2290 2291 if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) { 2292 messageUri = uiSendDraftMessage(accountId, extras); 2293 Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId); 2294 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) { 2295 messageUri = uiSaveDraftMessage(accountId, extras); 2296 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) { 2297 LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method); 2298 } else { 2299 LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method); 2300 } 2301 2302 final Bundle result; 2303 if (messageUri != null) { 2304 result = new Bundle(1); 2305 result.putParcelable(UIProvider.MessageColumns.URI, messageUri); 2306 } else { 2307 result = null; 2308 } 2309 2310 return result; 2311 } 2312 2313 private static void deleteBodyFiles(final Context c, final long messageId) 2314 throws IllegalStateException { 2315 final ContentValues emptyValues = new ContentValues(2); 2316 emptyValues.putNull(BodyColumns.HTML_CONTENT); 2317 emptyValues.putNull(BodyColumns.TEXT_CONTENT); 2318 writeBodyFiles(c, messageId, emptyValues); 2319 } 2320 2321 /** 2322 * Writes message bodies to disk, read from a set of ContentValues 2323 * 2324 * @param c Context for finding files 2325 * @param messageId id of message to write body for 2326 * @param cv {@link ContentValues} containing {@link BodyColumns#HTML_CONTENT} and/or 2327 * {@link BodyColumns#TEXT_CONTENT}. Inserting a null or empty value will delete the 2328 * associated text or html body file 2329 * @throws IllegalStateException 2330 */ 2331 private static void writeBodyFiles(final Context c, final long messageId, 2332 final ContentValues cv) throws IllegalStateException { 2333 if (cv.containsKey(BodyColumns.HTML_CONTENT)) { 2334 final String htmlContent = cv.getAsString(BodyColumns.HTML_CONTENT); 2335 try { 2336 writeBodyFile(c, messageId, "html", htmlContent); 2337 } catch (final IOException e) { 2338 throw new IllegalStateException("IOException while writing html body " + 2339 "for message id " + Long.toString(messageId), e); 2340 } 2341 } 2342 if (cv.containsKey(BodyColumns.TEXT_CONTENT)) { 2343 final String textContent = cv.getAsString(BodyColumns.TEXT_CONTENT); 2344 try { 2345 writeBodyFile(c, messageId, "txt", textContent); 2346 } catch (final IOException e) { 2347 throw new IllegalStateException("IOException while writing text body " + 2348 "for message id " + Long.toString(messageId), e); 2349 } 2350 } 2351 } 2352 2353 /** 2354 * Writes a message body file to disk 2355 * 2356 * @param c Context for finding files dir 2357 * @param messageId id of message to write body for 2358 * @param ext "html" or "txt" 2359 * @param content Body content to write to file, or null/empty to delete file 2360 * @throws IOException 2361 */ 2362 private static void writeBodyFile(final Context c, final long messageId, final String ext, 2363 final String content) throws IOException { 2364 final File textFile = getBodyFile(c, messageId, ext); 2365 if (TextUtils.isEmpty(content)) { 2366 if (!textFile.delete()) { 2367 LogUtils.v(LogUtils.TAG, "did not delete text body for %d", messageId); 2368 } 2369 } else { 2370 final FileWriter w = new FileWriter(textFile); 2371 try { 2372 w.write(content); 2373 } finally { 2374 w.close(); 2375 } 2376 } 2377 } 2378 2379 /** 2380 * Returns a {@link java.io.File} object pointing to the body content file for the message 2381 * 2382 * @param c Context for finding files dir 2383 * @param messageId id of message to locate 2384 * @param ext "html" or "txt" 2385 * @return File ready for operating upon 2386 */ 2387 protected static File getBodyFile(final Context c, final long messageId, final String ext) 2388 throws FileNotFoundException { 2389 if (!TextUtils.equals(ext, "html") && !TextUtils.equals(ext, "txt")) { 2390 throw new IllegalArgumentException("ext must be one of 'html' or 'txt'"); 2391 } 2392 long l1 = messageId / 100 % 100; 2393 long l2 = messageId % 100; 2394 final File dir = new File(c.getFilesDir(), 2395 "body/" + Long.toString(l1) + "/" + Long.toString(l2) + "/"); 2396 if (!dir.isDirectory() && !dir.mkdirs()) { 2397 throw new FileNotFoundException("Could not create directory for body file"); 2398 } 2399 return new File(dir, Long.toString(messageId) + "." + ext); 2400 } 2401 2402 @Override 2403 public ParcelFileDescriptor openFile(final Uri uri, final String mode) 2404 throws FileNotFoundException { 2405 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) { 2406 LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri)); 2407 } 2408 2409 final int match = findMatch(uri, "openFile"); 2410 switch (match) { 2411 case ATTACHMENTS_CACHED_FILE_ACCESS: 2412 // Parse the cache file path out from the uri 2413 final String cachedFilePath = 2414 uri.getQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM); 2415 2416 if (cachedFilePath != null) { 2417 // clearCallingIdentity means that the download manager will 2418 // check our permissions rather than the permissions of whatever 2419 // code is calling us. 2420 long binderToken = Binder.clearCallingIdentity(); 2421 try { 2422 LogUtils.d(TAG, "Opening attachment %s", cachedFilePath); 2423 return ParcelFileDescriptor.open( 2424 new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY); 2425 } finally { 2426 Binder.restoreCallingIdentity(binderToken); 2427 } 2428 } 2429 break; 2430 case BODY_HTML: { 2431 final long messageKey = Long.valueOf(uri.getLastPathSegment()); 2432 return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "html"), 2433 Utilities.parseMode(mode)); 2434 } 2435 case BODY_TEXT:{ 2436 final long messageKey = Long.valueOf(uri.getLastPathSegment()); 2437 return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "txt"), 2438 Utilities.parseMode(mode)); 2439 } 2440 } 2441 2442 throw new FileNotFoundException("unable to open file"); 2443 } 2444 2445 2446 /** 2447 * Returns the base notification URI for the given content type. 2448 * 2449 * @param match The type of content that was modified. 2450 */ 2451 private static Uri getBaseNotificationUri(int match) { 2452 Uri baseUri = null; 2453 switch (match) { 2454 case MESSAGE: 2455 case MESSAGE_ID: 2456 case SYNCED_MESSAGE_ID: 2457 baseUri = Message.NOTIFIER_URI; 2458 break; 2459 case ACCOUNT: 2460 case ACCOUNT_ID: 2461 baseUri = Account.NOTIFIER_URI; 2462 break; 2463 } 2464 return baseUri; 2465 } 2466 2467 /** 2468 * Sends a change notification to any cursors observers of the given base URI. The final 2469 * notification URI is dynamically built to contain the specified information. It will be 2470 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 2471 * upon the given values. 2472 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 2473 * If this is necessary, it can be added. However, due to the implementation of 2474 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 2475 * 2476 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 2477 * @param op Optional operation to be appended to the URI. 2478 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 2479 * appended to the base URI. 2480 */ 2481 private void sendNotifierChange(Uri baseUri, String op, String id) { 2482 if (baseUri == null) return; 2483 2484 // Append the operation, if specified 2485 if (op != null) { 2486 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 2487 } 2488 2489 long longId = 0L; 2490 try { 2491 longId = Long.valueOf(id); 2492 } catch (NumberFormatException ignore) {} 2493 if (longId > 0) { 2494 notifyUI(baseUri, id); 2495 } else { 2496 notifyUI(baseUri, null); 2497 } 2498 2499 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 2500 if (baseUri.equals(Message.NOTIFIER_URI)) { 2501 sendMessageListDataChangedNotification(); 2502 } 2503 } 2504 2505 private void sendMessageListDataChangedNotification() { 2506 final Context context = getContext(); 2507 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 2508 // Ideally this intent would contain information about which account changed, to limit the 2509 // updates to that particular account. Unfortunately, that information is not available in 2510 // sendNotifierChange(). 2511 context.sendBroadcast(intent); 2512 } 2513 2514 // We might have more than one thread trying to make its way through applyBatch() so the 2515 // notification coalescing needs to be thread-local to work correctly. 2516 private final ThreadLocal<Set<Uri>> mTLBatchNotifications = 2517 new ThreadLocal<Set<Uri>>(); 2518 2519 private Set<Uri> getBatchNotificationsSet() { 2520 return mTLBatchNotifications.get(); 2521 } 2522 2523 private void setBatchNotificationsSet(Set<Uri> batchNotifications) { 2524 mTLBatchNotifications.set(batchNotifications); 2525 } 2526 2527 @Override 2528 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2529 throws OperationApplicationException { 2530 /** 2531 * Collect notification URIs to notify at the end of batch processing. 2532 * These are populated by calls to notifyUI() by way of update(), insert() and delete() 2533 * calls made in super.applyBatch() 2534 */ 2535 setBatchNotificationsSet(Sets.<Uri>newHashSet()); 2536 Context context = getContext(); 2537 SQLiteDatabase db = getDatabase(context); 2538 db.beginTransaction(); 2539 try { 2540 ContentProviderResult[] results = super.applyBatch(operations); 2541 db.setTransactionSuccessful(); 2542 return results; 2543 } finally { 2544 db.endTransaction(); 2545 final Set<Uri> notifications = getBatchNotificationsSet(); 2546 setBatchNotificationsSet(null); 2547 for (final Uri uri : notifications) { 2548 context.getContentResolver().notifyChange(uri, null); 2549 } 2550 } 2551 } 2552 2553 public static interface EmailAttachmentService { 2554 /** 2555 * Notify the service that an attachment has changed. 2556 */ 2557 void attachmentChanged(final Context context, final long id, final int flags); 2558 } 2559 2560 private final EmailAttachmentService DEFAULT_ATTACHMENT_SERVICE = new EmailAttachmentService() { 2561 @Override 2562 public void attachmentChanged(final Context context, final long id, final int flags) { 2563 // The default implementation delegates to the real service. 2564 AttachmentService.attachmentChanged(context, id, flags); 2565 } 2566 }; 2567 private EmailAttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 2568 2569 // exposed for testing 2570 public void injectAttachmentService(final EmailAttachmentService attachmentService) { 2571 mAttachmentService = 2572 attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService; 2573 } 2574 2575 private Cursor notificationQuery(final Uri uri) { 2576 final SQLiteDatabase db = getDatabase(getContext()); 2577 final String accountId = uri.getLastPathSegment(); 2578 2579 final String sql = "SELECT " + MessageColumns.MAILBOX_KEY + ", " + 2580 "SUM(CASE " + MessageColumns.FLAG_READ + " WHEN 0 THEN 1 ELSE 0 END), " + 2581 "SUM(CASE " + MessageColumns.FLAG_SEEN + " WHEN 0 THEN 1 ELSE 0 END)\n" + 2582 "FROM " + Message.TABLE_NAME + "\n" + 2583 "WHERE " + MessageColumns.ACCOUNT_KEY + " = ?\n" + 2584 "GROUP BY " + MessageColumns.MAILBOX_KEY; 2585 2586 final String[] selectionArgs = {accountId}; 2587 2588 return db.rawQuery(sql, selectionArgs); 2589 } 2590 2591 public Cursor mostRecentMessageQuery(Uri uri) { 2592 SQLiteDatabase db = getDatabase(getContext()); 2593 String mailboxId = uri.getLastPathSegment(); 2594 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 2595 new String[] {mailboxId}); 2596 } 2597 2598 private Cursor getMailboxMessageCount(Uri uri) { 2599 SQLiteDatabase db = getDatabase(getContext()); 2600 String mailboxId = uri.getLastPathSegment(); 2601 return db.rawQuery("select count(*) from Message where mailboxKey=?", 2602 new String[] {mailboxId}); 2603 } 2604 2605 /** 2606 * Support for UnifiedEmail below 2607 */ 2608 2609 private static final String NOT_A_DRAFT_STRING = 2610 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 2611 2612 private static final String CONVERSATION_FLAGS = 2613 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2614 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + 2615 " ELSE 0 END + " + 2616 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + 2617 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + 2618 " ELSE 0 END + " + 2619 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + 2620 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + 2621 " ELSE 0 END"; 2622 2623 /** 2624 * Array of pre-defined account colors (legacy colors from old email app) 2625 */ 2626 private static final int[] ACCOUNT_COLORS = new int[] { 2627 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, 2628 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 2629 }; 2630 2631 private static final String CONVERSATION_COLOR = 2632 "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + 2633 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2634 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2635 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2636 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2637 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2638 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2639 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2640 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2641 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2642 " END"; 2643 2644 private static final String ACCOUNT_COLOR = 2645 "@CASE (" + AccountColumns._ID + " - 1) % " + ACCOUNT_COLORS.length + 2646 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2647 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2648 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2649 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2650 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2651 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2652 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2653 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2654 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2655 " END"; 2656 2657 /** 2658 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 2659 * conversation list in UnifiedEmail) 2660 */ 2661 private static ProjectionMap getMessageListMap() { 2662 if (sMessageListMap == null) { 2663 sMessageListMap = ProjectionMap.builder() 2664 .add(BaseColumns._ID, MessageColumns._ID) 2665 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 2666 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 2667 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 2668 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 2669 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) 2670 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 2671 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 2672 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 2673 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 2674 .add(UIProvider.ConversationColumns.SENDING_STATE, 2675 Integer.toString(ConversationSendingState.OTHER)) 2676 .add(UIProvider.ConversationColumns.PRIORITY, 2677 Integer.toString(ConversationPriority.LOW)) 2678 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 2679 .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN) 2680 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 2681 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) 2682 .add(UIProvider.ConversationColumns.ACCOUNT_URI, 2683 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2684 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 2685 .add(UIProvider.ConversationColumns.ORDER_KEY, MessageColumns.TIMESTAMP) 2686 .build(); 2687 } 2688 return sMessageListMap; 2689 } 2690 private static ProjectionMap sMessageListMap; 2691 2692 /** 2693 * Generate UIProvider draft type; note the test for "reply all" must come before "reply" 2694 */ 2695 private static final String MESSAGE_DRAFT_TYPE = 2696 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + 2697 ") !=0 THEN " + UIProvider.DraftType.COMPOSE + 2698 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL + 2699 ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + 2700 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + 2701 ") !=0 THEN " + UIProvider.DraftType.REPLY + 2702 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + 2703 ") !=0 THEN " + UIProvider.DraftType.FORWARD + 2704 " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; 2705 2706 private static final String MESSAGE_FLAGS = 2707 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2708 ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + 2709 " ELSE 0 END"; 2710 2711 /** 2712 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 2713 * UnifiedEmail 2714 */ 2715 private static ProjectionMap getMessageViewMap() { 2716 if (sMessageViewMap == null) { 2717 sMessageViewMap = ProjectionMap.builder() 2718 .add(BaseColumns._ID, Message.TABLE_NAME + "." + MessageColumns._ID) 2719 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 2720 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 2721 .add(UIProvider.MessageColumns.CONVERSATION_ID, 2722 uriWithFQId("uimessage", Message.TABLE_NAME)) 2723 .add(UIProvider.MessageColumns.SUBJECT, MessageColumns.SUBJECT) 2724 .add(UIProvider.MessageColumns.SNIPPET, MessageColumns.SNIPPET) 2725 .add(UIProvider.MessageColumns.FROM, MessageColumns.FROM_LIST) 2726 .add(UIProvider.MessageColumns.TO, MessageColumns.TO_LIST) 2727 .add(UIProvider.MessageColumns.CC, MessageColumns.CC_LIST) 2728 .add(UIProvider.MessageColumns.BCC, MessageColumns.BCC_LIST) 2729 .add(UIProvider.MessageColumns.REPLY_TO, MessageColumns.REPLY_TO_LIST) 2730 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 2731 .add(UIProvider.MessageColumns.BODY_HTML, null) // Loaded in EmailMessageCursor 2732 .add(UIProvider.MessageColumns.BODY_TEXT, null) // Loaded in EmailMessageCursor 2733 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 2734 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 2735 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 2736 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 2737 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 2738 uriWithFQId("uiattachments", Message.TABLE_NAME)) 2739 .add(UIProvider.MessageColumns.ATTACHMENT_BY_CID_URI, 2740 uriWithFQId("uiattachmentbycid", Message.TABLE_NAME)) 2741 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) 2742 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) 2743 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, 2744 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2745 .add(UIProvider.MessageColumns.STARRED, MessageColumns.FLAG_FAVORITE) 2746 .add(UIProvider.MessageColumns.READ, MessageColumns.FLAG_READ) 2747 .add(UIProvider.MessageColumns.SEEN, MessageColumns.FLAG_SEEN) 2748 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) 2749 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, 2750 Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) 2751 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, 2752 Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) 2753 .add(UIProvider.MessageColumns.VIA_DOMAIN, null) 2754 .add(UIProvider.MessageColumns.CLIPPED, "0") 2755 .add(UIProvider.MessageColumns.PERMALINK, null) 2756 .build(); 2757 } 2758 return sMessageViewMap; 2759 } 2760 private static ProjectionMap sMessageViewMap; 2761 2762 /** 2763 * Generate UIProvider folder capabilities from mailbox flags 2764 */ 2765 private static final String FOLDER_CAPABILITIES = 2766 "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 2767 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 2768 " ELSE 0 END"; 2769 2770 /** 2771 * Convert EmailProvider type to UIProvider type 2772 */ 2773 private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE 2774 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX 2775 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT 2776 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX 2777 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT 2778 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH 2779 + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM 2780 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED 2781 + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD 2782 + " WHEN " + Mailbox.TYPE_SEARCH + " THEN " 2783 + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH) 2784 + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; 2785 2786 private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE 2787 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_drawer_inbox_24dp 2788 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_drawer_drafts_24dp 2789 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_drawer_outbox_24dp 2790 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_drawer_sent_24dp 2791 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + R.drawable.ic_drawer_trash_24dp 2792 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_drawer_starred_24dp 2793 + " ELSE " + R.drawable.ic_drawer_folder_24dp + " END"; 2794 2795 /** 2796 * Local-only folders set totalCount < 0; such folders should substitute message count for 2797 * total count. 2798 * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types. 2799 */ 2800 private static final String TOTAL_COUNT = "CASE WHEN " 2801 + MailboxColumns.TOTAL_COUNT + "<0 OR " 2802 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR " 2803 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR " 2804 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH 2805 + " THEN " + MailboxColumns.MESSAGE_COUNT 2806 + " ELSE " + MailboxColumns.TOTAL_COUNT + " END"; 2807 2808 private static ProjectionMap getFolderListMap() { 2809 if (sFolderListMap == null) { 2810 sFolderListMap = ProjectionMap.builder() 2811 .add(BaseColumns._ID, MailboxColumns._ID) 2812 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) 2813 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 2814 .add(UIProvider.FolderColumns.NAME, "displayName") 2815 .add(UIProvider.FolderColumns.HAS_CHILDREN, 2816 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 2817 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 2818 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 2819 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2820 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2821 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2822 .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT) 2823 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH)) 2824 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2825 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2826 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 2827 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 2828 .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore")) 2829 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 2830 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY 2831 + "=" + Mailbox.NO_MAILBOX + " then NULL else " + 2832 uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end") 2833 /** 2834 * SELECT group_concat(fromList) FROM 2835 * (SELECT fromList FROM message WHERE mailboxKey=? AND flagRead=0 2836 * GROUP BY fromList ORDER BY timestamp DESC) 2837 */ 2838 .add(UIProvider.FolderColumns.UNREAD_SENDERS, 2839 "(SELECT group_concat(" + MessageColumns.FROM_LIST + ") FROM " + 2840 "(SELECT " + MessageColumns.FROM_LIST + " FROM " + Message.TABLE_NAME + 2841 " WHERE " + MessageColumns.MAILBOX_KEY + "=" + Mailbox.TABLE_NAME + "." + 2842 MailboxColumns._ID + " AND " + MessageColumns.FLAG_READ + "=0" + 2843 " GROUP BY " + MessageColumns.FROM_LIST + " ORDER BY " + 2844 MessageColumns.TIMESTAMP + " DESC))") 2845 .build(); 2846 } 2847 return sFolderListMap; 2848 } 2849 private static ProjectionMap sFolderListMap; 2850 2851 /** 2852 * Constructs the map of default entries for accounts. These values can be overridden in 2853 * {@link #genQueryAccount(String[], String)}. 2854 */ 2855 private static ProjectionMap getAccountListMap(Context context) { 2856 if (sAccountListMap == null) { 2857 final ProjectionMap.Builder builder = ProjectionMap.builder() 2858 .add(BaseColumns._ID, AccountColumns._ID) 2859 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2860 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders")) 2861 .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 2862 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2863 .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME, 2864 AccountColumns.EMAIL_ADDRESS) 2865 .add(UIProvider.AccountColumns.ACCOUNT_ID, 2866 AccountColumns.EMAIL_ADDRESS) 2867 .add(UIProvider.AccountColumns.SENDER_NAME, 2868 AccountColumns.SENDER_NAME) 2869 .add(UIProvider.AccountColumns.UNDO_URI, 2870 ("'content://" + EmailContent.AUTHORITY + "/uiundo'")) 2871 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2872 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2873 // TODO: Is provider version used? 2874 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2875 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2876 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, 2877 uriWithId("uirecentfolders")) 2878 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 2879 uriWithId("uidefaultrecentfolders")) 2880 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, 2881 AccountColumns.SIGNATURE) 2882 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 2883 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2884 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 2885 .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, 2886 Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) 2887 .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null); 2888 2889 final String feedbackUri = context.getString(R.string.email_feedback_uri); 2890 if (!TextUtils.isEmpty(feedbackUri)) { 2891 // This string needs to be in single quotes, as it will be used as a constant 2892 // in a sql expression 2893 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, 2894 "'" + feedbackUri + "'"); 2895 } 2896 2897 final String helpUri = context.getString(R.string.help_uri); 2898 if (!TextUtils.isEmpty(helpUri)) { 2899 // This string needs to be in single quotes, as it will be used as a constant 2900 // in a sql expression 2901 builder.add(UIProvider.AccountColumns.HELP_INTENT_URI, 2902 "'" + helpUri + "'"); 2903 } 2904 2905 sAccountListMap = builder.build(); 2906 } 2907 return sAccountListMap; 2908 } 2909 private static ProjectionMap sAccountListMap; 2910 2911 private static ProjectionMap getQuickResponseMap() { 2912 if (sQuickResponseMap == null) { 2913 sQuickResponseMap = ProjectionMap.builder() 2914 .add(UIProvider.QuickResponseColumns.TEXT, QuickResponseColumns.TEXT) 2915 .add(UIProvider.QuickResponseColumns.URI, 2916 "'" + combinedUriString("quickresponse", "") + "'||" 2917 + QuickResponseColumns._ID) 2918 .build(); 2919 } 2920 return sQuickResponseMap; 2921 } 2922 private static ProjectionMap sQuickResponseMap; 2923 2924 /** 2925 * The "ORDER BY" clause for top level folders 2926 */ 2927 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2928 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2929 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2930 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2931 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2932 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2933 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2934 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2935 + " ELSE 10 END" 2936 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2937 2938 /** 2939 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2940 */ 2941 private static ProjectionMap getAttachmentMap() { 2942 if (sAttachmentMap == null) { 2943 sAttachmentMap = ProjectionMap.builder() 2944 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2945 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2946 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2947 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2948 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2949 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2950 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, 2951 AttachmentColumns.UI_DOWNLOADED_SIZE) 2952 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2953 .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS) 2954 .build(); 2955 } 2956 return sAttachmentMap; 2957 } 2958 private static ProjectionMap sAttachmentMap; 2959 2960 /** 2961 * Generate the SELECT clause using a specified mapping and the original UI projection 2962 * @param map the ProjectionMap to use for this projection 2963 * @param projection the projection as sent by UnifiedEmail 2964 * @return a StringBuilder containing the SELECT expression for a SQLite query 2965 */ 2966 private static StringBuilder genSelect(ProjectionMap map, String[] projection) { 2967 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2968 } 2969 2970 private static StringBuilder genSelect(ProjectionMap map, String[] projection, 2971 ContentValues values) { 2972 final StringBuilder sb = new StringBuilder("SELECT "); 2973 boolean first = true; 2974 for (final String column: projection) { 2975 if (first) { 2976 first = false; 2977 } else { 2978 sb.append(','); 2979 } 2980 final String val; 2981 // First look at values; this is an override of default behavior 2982 if (values.containsKey(column)) { 2983 final String value = values.getAsString(column); 2984 if (value == null) { 2985 val = "NULL AS " + column; 2986 } else if (value.startsWith("@")) { 2987 val = value.substring(1) + " AS " + column; 2988 } else { 2989 val = DatabaseUtils.sqlEscapeString(value) + " AS " + column; 2990 } 2991 } else { 2992 // Now, get the standard value for the column from our projection map 2993 final String mapVal = map.get(column); 2994 // If we don't have the column, return "NULL AS <column>", and warn 2995 if (mapVal == null) { 2996 val = "NULL AS " + column; 2997 // Apparently there's a lot of these, so don't spam the log with warnings 2998 // LogUtils.w(TAG, "column " + column + " missing from projection map"); 2999 } else { 3000 val = mapVal; 3001 } 3002 } 3003 sb.append(val); 3004 } 3005 return sb; 3006 } 3007 3008 /** 3009 * Convenience method to create a Uri string given the "type" of query; we append the type 3010 * of the query and the id column name (_id) 3011 * 3012 * @param type the "type" of the query, as defined by our UriMatcher definitions 3013 * @return a Uri string 3014 */ 3015 private static String uriWithId(String type) { 3016 return uriWithColumn(type, BaseColumns._ID); 3017 } 3018 3019 /** 3020 * Convenience method to create a Uri string given the "type" of query; we append the type 3021 * of the query and the passed in column name 3022 * 3023 * @param type the "type" of the query, as defined by our UriMatcher definitions 3024 * @param columnName the column in the table being queried 3025 * @return a Uri string 3026 */ 3027 private static String uriWithColumn(String type, String columnName) { 3028 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 3029 } 3030 3031 /** 3032 * Convenience method to create a Uri string given the "type" of query and the table name to 3033 * which it applies; we append the type of the query and the fully qualified (FQ) id column 3034 * (i.e. including the table name); we need this for join queries where _id would otherwise 3035 * be ambiguous 3036 * 3037 * @param type the "type" of the query, as defined by our UriMatcher definitions 3038 * @param tableName the name of the table whose _id is referred to 3039 * @return a Uri string 3040 */ 3041 private static String uriWithFQId(String type, String tableName) { 3042 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 3043 } 3044 3045 // Regex that matches start of img tag. '<(?i)img\s+'. 3046 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 3047 3048 /** 3049 * Class that holds the sqlite query and the attachment (JSON) value (which might be null) 3050 */ 3051 private static class MessageQuery { 3052 final String query; 3053 final String attachmentJson; 3054 3055 MessageQuery(String _query, String _attachmentJson) { 3056 query = _query; 3057 attachmentJson = _attachmentJson; 3058 } 3059 } 3060 3061 /** 3062 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 3063 * 3064 * @param uiProjection as passed from UnifiedEmail 3065 * @return the SQLite query to be executed on the EmailProvider database 3066 */ 3067 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 3068 Context context = getContext(); 3069 long messageId = Long.parseLong(id); 3070 Message msg = Message.restoreMessageWithId(context, messageId); 3071 ContentValues values = new ContentValues(); 3072 String attachmentJson = null; 3073 if (msg != null) { 3074 Body body = Body.restoreBodyWithMessageId(context, messageId); 3075 if (body != null) { 3076 if (body.mHtmlContent != null) { 3077 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 3078 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 3079 } 3080 } 3081 } 3082 Address[] fromList = Address.fromHeader(msg.mFrom); 3083 int autoShowImages = 0; 3084 final MailPrefs mailPrefs = MailPrefs.get(context); 3085 for (Address sender : fromList) { 3086 final String email = sender.getAddress(); 3087 if (mailPrefs.getDisplayImagesFromSender(email)) { 3088 autoShowImages = 1; 3089 break; 3090 } 3091 } 3092 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 3093 // Add attachments... 3094 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 3095 if (atts.length > 0) { 3096 ArrayList<com.android.mail.providers.Attachment> uiAtts = 3097 new ArrayList<com.android.mail.providers.Attachment>(); 3098 for (Attachment att : atts) { 3099 // TODO: This code is intended to strip out any inlined attachments (which 3100 // would have a non-null contentId) so that they will not display at the bottom 3101 // along with the non-inlined attachments. 3102 // The problem is that the UI_ATTACHMENTS query does not behave the same way, 3103 // which causes crazy formatting. 3104 // There is an open question here, should attachments that are inlined 3105 // ALSO appear in the list of attachments at the bottom with the non-inlined 3106 // attachments? 3107 // Either way, the two queries need to behave the same way. 3108 // As of now, they will. If we decide to stop this, then we need to enable 3109 // the code below, and then also make the UI_ATTACHMENTS query behave 3110 // the same way. 3111 // 3112 // if (att.mContentId != null && att.getContentUri() != null) { 3113 // continue; 3114 // } 3115 com.android.mail.providers.Attachment uiAtt = 3116 new com.android.mail.providers.Attachment(); 3117 uiAtt.setName(att.mFileName); 3118 uiAtt.setContentType(att.mMimeType); 3119 uiAtt.size = (int) att.mSize; 3120 uiAtt.uri = uiUri("uiattachment", att.mId); 3121 uiAtt.flags = att.mFlags; 3122 uiAtts.add(uiAtt); 3123 } 3124 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 3125 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 3126 } 3127 if (msg.mDraftInfo != 0) { 3128 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 3129 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 3130 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 3131 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 3132 } 3133 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 3134 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 3135 "content://ui.email2.android.com/event/" + msg.mId); 3136 } 3137 /** 3138 * HACK: override the attachment uri to contain a query parameter 3139 * This forces the message footer to reload the attachment display when the message is 3140 * fully loaded. 3141 */ 3142 final Uri attachmentListUri = uiUri("uiattachments", messageId).buildUpon() 3143 .appendQueryParameter("MessageLoaded", 3144 msg.mFlagLoaded == Message.FLAG_LOADED_COMPLETE ? "true" : "false") 3145 .build(); 3146 values.put(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, attachmentListUri.toString()); 3147 } 3148 StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); 3149 sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + 3150 " ON " + BodyColumns.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + 3151 MessageColumns._ID + 3152 " WHERE " + Message.TABLE_NAME + "." + MessageColumns._ID + "=?"); 3153 String sql = sb.toString(); 3154 return new MessageQuery(sql, attachmentJson); 3155 } 3156 3157 private static void appendConversationInfoColumns(final StringBuilder stringBuilder) { 3158 // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :( 3159 // There may be a better way to do this, but since the projection is specified by the 3160 // unified UI code, it can't ask for these columns. 3161 stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME) 3162 .append(',').append(MessageColumns.FROM_LIST) 3163 .append(',').append(MessageColumns.TO_LIST); 3164 } 3165 3166 /** 3167 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 3168 * 3169 * @param uiProjection as passed from UnifiedEmail 3170 * @param unseenOnly <code>true</code> to only return unseen messages 3171 * @return the SQLite query to be executed on the EmailProvider database 3172 */ 3173 private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { 3174 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 3175 appendConversationInfoColumns(sb); 3176 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 3177 Message.FLAG_LOADED_SELECTION + " AND " + 3178 MessageColumns.MAILBOX_KEY + "=? "); 3179 if (unseenOnly) { 3180 sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); 3181 sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 "); 3182 } 3183 sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC "); 3184 sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMIT); 3185 return sb.toString(); 3186 } 3187 3188 /** 3189 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 3190 * 3191 * @param uiProjection as passed from UnifiedEmail 3192 * @param mailboxId the id of the virtual mailbox 3193 * @param unseenOnly <code>true</code> to only return unseen messages 3194 * @return the SQLite query to be executed on the EmailProvider database 3195 */ 3196 private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 3197 long mailboxId, final boolean unseenOnly) { 3198 ContentValues values = new ContentValues(); 3199 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 3200 final int virtualMailboxId = getVirtualMailboxType(mailboxId); 3201 final String[] selectionArgs; 3202 StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); 3203 appendConversationInfoColumns(sb); 3204 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 3205 Message.FLAG_LOADED_SELECTION + " AND "); 3206 if (isCombinedMailbox(mailboxId)) { 3207 if (unseenOnly) { 3208 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND "); 3209 sb.append(MessageColumns.FLAG_READ).append("=0 AND "); 3210 } 3211 selectionArgs = null; 3212 } else { 3213 if (virtualMailboxId == Mailbox.TYPE_INBOX) { 3214 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 3215 } 3216 sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND "); 3217 selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)}; 3218 } 3219 switch (getVirtualMailboxType(mailboxId)) { 3220 case Mailbox.TYPE_INBOX: 3221 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns._ID + 3222 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 3223 "=" + Mailbox.TYPE_INBOX + ")"); 3224 break; 3225 case Mailbox.TYPE_STARRED: 3226 sb.append(MessageColumns.FLAG_FAVORITE + "=1"); 3227 break; 3228 case Mailbox.TYPE_UNREAD: 3229 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY + 3230 " NOT IN (SELECT " + MailboxColumns._ID + " FROM " + Mailbox.TABLE_NAME + 3231 " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")"); 3232 break; 3233 default: 3234 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 3235 } 3236 sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); 3237 return db.rawQuery(sb.toString(), selectionArgs); 3238 } 3239 3240 /** 3241 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 3242 * 3243 * @param uiProjection as passed from UnifiedEmail 3244 * @return the SQLite query to be executed on the EmailProvider database 3245 */ 3246 private static String genQueryConversation(String[] uiProjection) { 3247 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 3248 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + MessageColumns._ID + "=?"); 3249 return sb.toString(); 3250 } 3251 3252 /** 3253 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 3254 * 3255 * @param uiProjection as passed from UnifiedEmail 3256 * @return the SQLite query to be executed on the EmailProvider database 3257 */ 3258 private static String genQueryAccountMailboxes(String[] uiProjection) { 3259 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3260 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 3261 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 3262 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 3263 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 3264 sb.append(MAILBOX_ORDER_BY); 3265 return sb.toString(); 3266 } 3267 3268 /** 3269 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 3270 * sorted by the name as it appears in a hierarchical listing 3271 * 3272 * @param uiProjection as passed from UnifiedEmail 3273 * @return the SQLite query to be executed on the EmailProvider database 3274 */ 3275 private static String genQueryAccountAllMailboxes(String[] uiProjection) { 3276 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3277 // Use a derived column to choose either hierarchicalName or displayName 3278 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 3279 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 3280 " end as h_name"); 3281 // Order by the derived column 3282 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 3283 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 3284 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 3285 " ORDER BY h_name"); 3286 return sb.toString(); 3287 } 3288 3289 /** 3290 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 3291 * 3292 * @param uiProjection as passed from UnifiedEmail 3293 * @return the SQLite query to be executed on the EmailProvider database 3294 */ 3295 private static String genQueryRecentMailboxes(String[] uiProjection) { 3296 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3297 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 3298 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 3299 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 3300 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 3301 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 3302 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 3303 return sb.toString(); 3304 } 3305 3306 private int getFolderCapabilities(EmailServiceInfo info, int mailboxType, long mailboxId) { 3307 // Special case for Search folders: only permit delete, do not try to give any other caps. 3308 if (mailboxType == Mailbox.TYPE_SEARCH) { 3309 return UIProvider.FolderCapabilities.DELETE; 3310 } 3311 3312 // All folders support delete, except drafts. 3313 int caps = 0; 3314 if (mailboxType != Mailbox.TYPE_DRAFTS) { 3315 caps = UIProvider.FolderCapabilities.DELETE; 3316 } 3317 if (info != null && info.offerLookback) { 3318 // Protocols supporting lookback support settings 3319 caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 3320 } 3321 3322 if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH || 3323 mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) { 3324 // If the mailbox can accept moved mail, report that as well 3325 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 3326 caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION; 3327 } 3328 3329 // For trash, we don't allow undo 3330 if (mailboxType == Mailbox.TYPE_TRASH) { 3331 caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 3332 UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION | 3333 UIProvider.FolderCapabilities.DELETE | 3334 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; 3335 } 3336 if (isVirtualMailbox(mailboxId)) { 3337 caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; 3338 } 3339 3340 // If we don't know the protocol or the protocol doesn't support it, don't allow moving 3341 // messages 3342 if (info == null || !info.offerMoveTo) { 3343 caps &= ~UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES & 3344 ~UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION & 3345 ~UIProvider.FolderCapabilities.ALLOWS_MOVE_TO_INBOX; 3346 } 3347 3348 // If the mailbox stores outgoing mail, show recipients instead of senders 3349 // (however the Drafts folder shows neither senders nor recipients... just the word "Draft") 3350 if (mailboxType == Mailbox.TYPE_OUTBOX || mailboxType == Mailbox.TYPE_SENT) { 3351 caps |= UIProvider.FolderCapabilities.SHOW_RECIPIENTS; 3352 } 3353 3354 return caps; 3355 } 3356 3357 /** 3358 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 3359 * 3360 * @param uiProjection as passed from UnifiedEmail 3361 * @return the SQLite query to be executed on the EmailProvider database 3362 */ 3363 private String genQueryMailbox(String[] uiProjection, String id) { 3364 long mailboxId = Long.parseLong(id); 3365 ContentValues values = new ContentValues(3); 3366 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 3367 // "load more" is valid for search results 3368 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 3369 uiUriString("uiloadmore", mailboxId)); 3370 values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE); 3371 } else { 3372 Context context = getContext(); 3373 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3374 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 3375 if (mailbox != null) { 3376 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 3377 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 3378 // All folders support delete 3379 if (info != null && info.offerLoadMore) { 3380 // "load more" is valid for protocols not supporting "lookback" 3381 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 3382 uiUriString("uiloadmore", mailboxId)); 3383 } 3384 values.put(UIProvider.FolderColumns.CAPABILITIES, 3385 getFolderCapabilities(info, mailbox.mType, mailboxId)); 3386 // The persistent id is used to form a filename, so we must ensure that it doesn't 3387 // include illegal characters (such as '/'). Only perform the encoding if this 3388 // query wants the persistent id. 3389 boolean shouldEncodePersistentId = false; 3390 if (uiProjection == null) { 3391 shouldEncodePersistentId = true; 3392 } else { 3393 for (final String column : uiProjection) { 3394 if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) { 3395 shouldEncodePersistentId = true; 3396 break; 3397 } 3398 } 3399 } 3400 if (shouldEncodePersistentId) { 3401 values.put(UIProvider.FolderColumns.PERSISTENT_ID, 3402 Base64.encodeToString(mailbox.mServerId.getBytes(), 3403 Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); 3404 } 3405 } 3406 } 3407 StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); 3408 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns._ID + "=?"); 3409 return sb.toString(); 3410 } 3411 3412 public static final String LEGACY_AUTHORITY = "ui.email.android.com"; 3413 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY); 3414 3415 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 3416 3417 private static String getExternalUriString(String segment, String account) { 3418 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 3419 .appendQueryParameter("account", account).build().toString(); 3420 } 3421 3422 private static String getExternalUriStringEmail2(String segment, String account) { 3423 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 3424 .appendQueryParameter("account", account).build().toString(); 3425 } 3426 3427 private static String getBits(int bitField) { 3428 StringBuilder sb = new StringBuilder(" "); 3429 for (int i = 0; i < 32; i++, bitField >>= 1) { 3430 if ((bitField & 1) != 0) { 3431 sb.append(i) 3432 .append(" "); 3433 } 3434 } 3435 return sb.toString(); 3436 } 3437 3438 private static int getCapabilities(Context context, final Account account) { 3439 if (account == null) { 3440 return 0; 3441 } 3442 // Account capabilities are based on protocol -- different protocols (and, for EAS, 3443 // different protocol versions) support different feature sets. 3444 final String protocol = account.getProtocol(context); 3445 int capabilities; 3446 if (TextUtils.equals(context.getString(R.string.protocol_imap), protocol) || 3447 TextUtils.equals(context.getString(R.string.protocol_legacy_imap), protocol)) { 3448 capabilities = AccountCapabilities.SYNCABLE_FOLDERS | 3449 AccountCapabilities.SERVER_SEARCH | 3450 AccountCapabilities.FOLDER_SERVER_SEARCH | 3451 AccountCapabilities.UNDO | 3452 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 3453 } else if (TextUtils.equals(context.getString(R.string.protocol_pop3), protocol)) { 3454 capabilities = AccountCapabilities.UNDO | 3455 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 3456 } else if (TextUtils.equals(context.getString(R.string.protocol_eas), protocol)) { 3457 final String easVersion = account.mProtocolVersion; 3458 double easVersionDouble = 2.5D; 3459 if (easVersion != null) { 3460 try { 3461 easVersionDouble = Double.parseDouble(easVersion); 3462 } catch (final NumberFormatException e) { 3463 // Use the default (lowest) set of capabilities. 3464 } 3465 } 3466 if (easVersionDouble >= 12.0D) { 3467 capabilities = AccountCapabilities.SYNCABLE_FOLDERS | 3468 AccountCapabilities.SERVER_SEARCH | 3469 AccountCapabilities.FOLDER_SERVER_SEARCH | 3470 AccountCapabilities.SMART_REPLY | 3471 AccountCapabilities.UNDO | 3472 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 3473 } else { 3474 capabilities = AccountCapabilities.SYNCABLE_FOLDERS | 3475 AccountCapabilities.SMART_REPLY | 3476 AccountCapabilities.UNDO | 3477 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 3478 } 3479 } else { 3480 LogUtils.w(TAG, "Unknown protocol for account %d", account.getId()); 3481 return 0; 3482 } 3483 LogUtils.d(TAG, "getCapabilities() for %d (protocol %s): 0x%x %s", account.getId(), protocol, 3484 capabilities, getBits(capabilities)); 3485 3486 // If the configuration states that feedback is supported, add that capability 3487 final Resources res = context.getResources(); 3488 if (res.getBoolean(R.bool.feedback_supported)) { 3489 capabilities |= AccountCapabilities.SEND_FEEDBACK; 3490 } 3491 3492 // If we can find a help URL then add the Help capability 3493 if (!TextUtils.isEmpty(context.getResources().getString(R.string.help_uri))) { 3494 capabilities |= AccountCapabilities.HELP_CONTENT; 3495 } 3496 3497 capabilities |= AccountCapabilities.EMPTY_TRASH; 3498 3499 // TODO: Should this be stored per-account, or some other mechanism? 3500 capabilities |= AccountCapabilities.NESTED_FOLDERS; 3501 3502 // the client is permitted to sanitize HTML emails for all Email accounts 3503 capabilities |= AccountCapabilities.CLIENT_SANITIZED_HTML; 3504 3505 return capabilities; 3506 } 3507 3508 /** 3509 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 3510 * 3511 * @param uiProjection as passed from UnifiedEmail 3512 * @param id account row ID 3513 * @return the SQLite query to be executed on the EmailProvider database 3514 */ 3515 private String genQueryAccount(String[] uiProjection, String id) { 3516 final ContentValues values = new ContentValues(); 3517 final long accountId = Long.parseLong(id); 3518 final Context context = getContext(); 3519 3520 EmailServiceInfo info = null; 3521 3522 // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null. 3523 final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection); 3524 3525 final Account account = Account.restoreAccountWithId(context, accountId); 3526 if (account == null) { 3527 LogUtils.d(TAG, "Account %d not found during genQueryAccount", accountId); 3528 } 3529 if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { 3530 // Get account capabilities from the service 3531 values.put(UIProvider.AccountColumns.CAPABILITIES, 3532 (account == null ? 0 : getCapabilities(context, account))); 3533 } 3534 if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 3535 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 3536 getExternalUriString("settings", id)); 3537 } 3538 if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { 3539 values.put(UIProvider.AccountColumns.COMPOSE_URI, 3540 getExternalUriStringEmail2("compose", id)); 3541 } 3542 if (projectionColumns.contains(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI)) { 3543 values.put(UIProvider.AccountColumns.REAUTHENTICATION_INTENT_URI, 3544 getIncomingSettingsUri(accountId).toString()); 3545 } 3546 if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { 3547 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 3548 } 3549 if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { 3550 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 3551 } 3552 3553 // TODO: if we're getting the values out of MailPrefs then we don't need to be passing the 3554 // values this way 3555 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 3556 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 3557 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE,