EmailProvider.java revision 7fdde9bb4a24e931618a7a64227e2194c89034da
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 17package com.android.email.provider; 18 19import android.appwidget.AppWidgetManager; 20import android.content.ComponentName; 21import android.content.ContentProvider; 22import android.content.ContentProviderOperation; 23import android.content.ContentProviderResult; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.OperationApplicationException; 30import android.content.UriMatcher; 31import android.database.ContentObserver; 32import android.database.Cursor; 33import android.database.CursorWrapper; 34import android.database.MatrixCursor; 35import android.database.MergeCursor; 36import android.database.sqlite.SQLiteDatabase; 37import android.database.sqlite.SQLiteException; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Parcel; 41import android.os.RemoteException; 42import android.provider.BaseColumns; 43import android.text.TextUtils; 44import android.util.Log; 45 46import com.android.common.content.ProjectionMap; 47import com.android.email.NotificationController; 48import com.android.email.Preferences; 49import com.android.email.R; 50import com.android.email.SecurityPolicy; 51import com.android.email.provider.ContentCache.CacheToken; 52import com.android.email.service.AttachmentDownloadService; 53import com.android.email.service.EmailServiceUtils; 54import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 55import com.android.email2.ui.MailActivityEmail; 56import com.android.emailcommon.Logging; 57import com.android.emailcommon.mail.Address; 58import com.android.emailcommon.provider.Account; 59import com.android.emailcommon.provider.EmailContent; 60import com.android.emailcommon.provider.EmailContent.AccountColumns; 61import com.android.emailcommon.provider.EmailContent.Attachment; 62import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 63import com.android.emailcommon.provider.EmailContent.Body; 64import com.android.emailcommon.provider.EmailContent.BodyColumns; 65import com.android.emailcommon.provider.EmailContent.MailboxColumns; 66import com.android.emailcommon.provider.EmailContent.Message; 67import com.android.emailcommon.provider.EmailContent.MessageColumns; 68import com.android.emailcommon.provider.EmailContent.PolicyColumns; 69import com.android.emailcommon.provider.EmailContent.SyncColumns; 70import com.android.emailcommon.provider.HostAuth; 71import com.android.emailcommon.provider.Mailbox; 72import com.android.emailcommon.provider.Policy; 73import com.android.emailcommon.provider.QuickResponse; 74import com.android.emailcommon.service.EmailServiceProxy; 75import com.android.emailcommon.service.IEmailService; 76import com.android.emailcommon.service.IEmailServiceCallback; 77import com.android.emailcommon.service.SearchParams; 78import com.android.emailcommon.utility.AttachmentUtilities; 79import com.android.emailcommon.utility.Utility; 80import com.android.ex.photo.provider.PhotoContract; 81import com.android.mail.providers.Folder; 82import com.android.mail.providers.FolderList; 83import com.android.mail.providers.UIProvider; 84import com.android.mail.providers.UIProvider.AccountCapabilities; 85import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 86import com.android.mail.providers.UIProvider.ConversationPriority; 87import com.android.mail.providers.UIProvider.ConversationSendingState; 88import com.android.mail.providers.UIProvider.DraftType; 89import com.android.mail.providers.UIProvider.Swipe; 90import com.android.mail.utils.LogUtils; 91import com.android.mail.utils.MatrixCursorWithCachedColumns; 92import com.android.mail.utils.MatrixCursorWithExtra; 93import com.android.mail.utils.Utils; 94import com.android.mail.widget.BaseWidgetProvider; 95import com.android.mail.widget.WidgetProvider; 96import com.google.common.annotations.VisibleForTesting; 97import com.google.common.collect.ImmutableMap; 98import com.google.common.collect.ImmutableSet; 99 100import java.io.File; 101import java.io.FileDescriptor; 102import java.io.PrintWriter; 103import java.util.ArrayList; 104import java.util.Arrays; 105import java.util.Collection; 106import java.util.HashMap; 107import java.util.List; 108import java.util.Map; 109import java.util.Set; 110import java.util.regex.Pattern; 111 112/** 113 * @author mblank 114 * 115 */ 116public class EmailProvider extends ContentProvider { 117 118 private static final String TAG = "EmailProvider"; 119 120 public static String EMAIL_APP_MIME_TYPE; 121 122 protected static final String DATABASE_NAME = "EmailProvider.db"; 123 protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 124 protected static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 125 126 public static final String ACTION_ATTACHMENT_UPDATED = "com.android.email.ATTACHMENT_UPDATED"; 127 public static final String ATTACHMENT_UPDATED_EXTRA_FLAGS = 128 "com.android.email.ATTACHMENT_UPDATED_FLAGS"; 129 130 /** 131 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 132 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 133 * is NOT the preferred way of getting notification. 134 */ 135 public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 136 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 137 138 public static final String EMAIL_MESSAGE_MIME_TYPE = 139 "vnd.android.cursor.item/email-message"; 140 public static final String EMAIL_ATTACHMENT_MIME_TYPE = 141 "vnd.android.cursor.item/email-attachment"; 142 143 /** Appended to the notification URI for delete operations */ 144 public static final String NOTIFICATION_OP_DELETE = "delete"; 145 /** Appended to the notification URI for insert operations */ 146 public static final String NOTIFICATION_OP_INSERT = "insert"; 147 /** Appended to the notification URI for update operations */ 148 public static final String NOTIFICATION_OP_UPDATE = "update"; 149 150 // Definitions for our queries looking for orphaned messages 151 private static final String[] ORPHANS_PROJECTION 152 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 153 private static final int ORPHANS_ID = 0; 154 private static final int ORPHANS_MAILBOX_KEY = 1; 155 156 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 157 158 // This is not a hard limit on accounts, per se, but beyond this, we can't guarantee that all 159 // critical mailboxes, host auth's, accounts, and policies are cached 160 private static final int MAX_CACHED_ACCOUNTS = 16; 161 // Inbox, Drafts, Sent, Outbox, Trash, and Search (these boxes are cached when possible) 162 private static final int NUM_ALWAYS_CACHED_MAILBOXES = 6; 163 164 // We'll cache the following four tables; sizes are best estimates of effective values 165 private final ContentCache mCacheAccount = 166 new ContentCache("Account", Account.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 167 private final ContentCache mCacheHostAuth = 168 new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS * 2); 169 /*package*/ final ContentCache mCacheMailbox = 170 new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 171 MAX_CACHED_ACCOUNTS * (NUM_ALWAYS_CACHED_MAILBOXES + 2)); 172 private final ContentCache mCacheMessage = 173 new ContentCache("Message", Message.CONTENT_PROJECTION, 8); 174 private final ContentCache mCachePolicy = 175 new ContentCache("Policy", Policy.CONTENT_PROJECTION, MAX_CACHED_ACCOUNTS); 176 177 private static final int ACCOUNT_BASE = 0; 178 private static final int ACCOUNT = ACCOUNT_BASE; 179 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 180 private static final int ACCOUNT_ID_ADD_TO_FIELD = ACCOUNT_BASE + 2; 181 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 3; 182 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 4; 183 private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5; 184 private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 6; 185 private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 7; 186 private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 8; 187 188 private static final int MAILBOX_BASE = 0x1000; 189 private static final int MAILBOX = MAILBOX_BASE; 190 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 191 private static final int MAILBOX_ID_FROM_ACCOUNT_AND_TYPE = MAILBOX_BASE + 2; 192 private static final int MAILBOX_ID_ADD_TO_FIELD = MAILBOX_BASE + 3; 193 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 4; 194 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 5; 195 196 private static final int MESSAGE_BASE = 0x2000; 197 private static final int MESSAGE = MESSAGE_BASE; 198 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 199 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 200 private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3; 201 202 private static final int ATTACHMENT_BASE = 0x3000; 203 private static final int ATTACHMENT = ATTACHMENT_BASE; 204 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 205 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 206 207 private static final int HOSTAUTH_BASE = 0x4000; 208 private static final int HOSTAUTH = HOSTAUTH_BASE; 209 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 210 211 private static final int UPDATED_MESSAGE_BASE = 0x5000; 212 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 213 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 214 215 private static final int DELETED_MESSAGE_BASE = 0x6000; 216 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 217 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 218 219 private static final int POLICY_BASE = 0x7000; 220 private static final int POLICY = POLICY_BASE; 221 private static final int POLICY_ID = POLICY_BASE + 1; 222 223 private static final int QUICK_RESPONSE_BASE = 0x8000; 224 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 225 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 226 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 227 228 private static final int UI_BASE = 0x9000; 229 private static final int UI_FOLDERS = UI_BASE; 230 private static final int UI_SUBFOLDERS = UI_BASE + 1; 231 private static final int UI_MESSAGES = UI_BASE + 2; 232 private static final int UI_MESSAGE = UI_BASE + 3; 233 private static final int UI_SENDMAIL = UI_BASE + 4; 234 private static final int UI_UNDO = UI_BASE + 5; 235 private static final int UI_SAVEDRAFT = UI_BASE + 6; 236 private static final int UI_UPDATEDRAFT = UI_BASE + 7; 237 private static final int UI_SENDDRAFT = UI_BASE + 8; 238 private static final int UI_FOLDER_REFRESH = UI_BASE + 9; 239 private static final int UI_FOLDER = UI_BASE + 10; 240 private static final int UI_ACCOUNT = UI_BASE + 11; 241 private static final int UI_ACCTS = UI_BASE + 12; 242 private static final int UI_ATTACHMENTS = UI_BASE + 13; 243 private static final int UI_ATTACHMENT = UI_BASE + 14; 244 private static final int UI_SEARCH = UI_BASE + 15; 245 private static final int UI_ACCOUNT_DATA = UI_BASE + 16; 246 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 17; 247 private static final int UI_CONVERSATION = UI_BASE + 18; 248 private static final int UI_RECENT_FOLDERS = UI_BASE + 19; 249 private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 20; 250 private static final int UI_ALL_FOLDERS = UI_BASE + 21; 251 252 // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS 253 private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; 254 255 // DO NOT CHANGE BODY_BASE!! 256 private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; 257 private static final int BODY = BODY_BASE; 258 private static final int BODY_ID = BODY_BASE + 1; 259 260 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 261 262 // TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000, 263 // MESSAGE_BASE = 0x1000, etc.) 264 private static final String[] TABLE_NAMES = { 265 Account.TABLE_NAME, 266 Mailbox.TABLE_NAME, 267 Message.TABLE_NAME, 268 Attachment.TABLE_NAME, 269 HostAuth.TABLE_NAME, 270 Message.UPDATED_TABLE_NAME, 271 Message.DELETED_TABLE_NAME, 272 Policy.TABLE_NAME, 273 QuickResponse.TABLE_NAME, 274 null, // UI 275 Body.TABLE_NAME, 276 }; 277 278 // CONTENT_CACHES MUST remain in the order of the BASE constants above 279 private final ContentCache[] mContentCaches = { 280 mCacheAccount, 281 mCacheMailbox, 282 mCacheMessage, 283 null, // Attachment 284 mCacheHostAuth, 285 null, // Updated message 286 null, // Deleted message 287 mCachePolicy, 288 null, // Quick response 289 null, // Body 290 null // UI 291 }; 292 293 // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above 294 private static final String[][] CACHE_PROJECTIONS = { 295 Account.CONTENT_PROJECTION, 296 Mailbox.CONTENT_PROJECTION, 297 Message.CONTENT_PROJECTION, 298 null, // Attachment 299 HostAuth.CONTENT_PROJECTION, 300 null, // Updated message 301 null, // Deleted message 302 Policy.CONTENT_PROJECTION, 303 null, // Quick response 304 null, // Body 305 null // UI 306 }; 307 308 private static UriMatcher sURIMatcher = null; 309 310 private static final String MAILBOX_PRE_CACHE_SELECTION = MailboxColumns.TYPE + " IN (" + 311 Mailbox.TYPE_INBOX + "," + Mailbox.TYPE_DRAFTS + "," + Mailbox.TYPE_TRASH + "," + 312 Mailbox.TYPE_SENT + "," + Mailbox.TYPE_SEARCH + "," + Mailbox.TYPE_OUTBOX + ")"; 313 314 /** 315 * Let's only generate these SQL strings once, as they are used frequently 316 * Note that this isn't relevant for table creation strings, since they are used only once 317 */ 318 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 319 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 320 EmailContent.RECORD_ID + '='; 321 322 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 323 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 324 325 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 326 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 327 EmailContent.RECORD_ID + '='; 328 329 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 330 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 331 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 332 Message.TABLE_NAME + ')'; 333 334 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 335 " where " + BodyColumns.MESSAGE_KEY + '='; 336 337 private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; 338 339 private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 340 private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); 341 342 public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 343 344 // For undo handling 345 private int mLastSequence = -1; 346 private ArrayList<ContentProviderOperation> mLastSequenceOps = 347 new ArrayList<ContentProviderOperation>(); 348 349 // Query parameter indicating the command came from UIProvider 350 private static final String IS_UIPROVIDER = "is_uiprovider"; 351 352 private static final String SWIPE_DELETE = Integer.toString(Swipe.DELETE); 353 private static final String SWIPE_DISABLED = Integer.toString(Swipe.DISABLED); 354 355 356 /** 357 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 358 * @param uri the Uri to match 359 * @return the match value 360 */ 361 private static int findMatch(Uri uri, String methodName) { 362 int match = sURIMatcher.match(uri); 363 if (match < 0) { 364 throw new IllegalArgumentException("Unknown uri: " + uri); 365 } else if (Logging.LOGD) { 366 Log.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 367 } 368 return match; 369 } 370 371 public static Uri INTEGRITY_CHECK_URI; 372 public static Uri ACCOUNT_BACKUP_URI; 373 public static Uri FOLDER_STATUS_URI; 374 public static Uri FOLDER_REFRESH_URI; 375 376 private SQLiteDatabase mDatabase; 377 private SQLiteDatabase mBodyDatabase; 378 379 public static Uri uiUri(String type, long id) { 380 return Uri.parse(uiUriString(type, id)); 381 } 382 383 /** 384 * Creates a URI string from a database ID (guaranteed to be unique). 385 * @param type of the resource: uifolder, message, etc. 386 * @param id the id of the resource. 387 * @return 388 */ 389 public static String uiUriString(String type, long id) { 390 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 391 } 392 393 /** 394 * Orphan record deletion utility. Generates a sqlite statement like: 395 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 396 * @param db the EmailProvider database 397 * @param table the table whose orphans are to be removed 398 * @param column the column deletion will be based on 399 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 400 * @param foreignTable the foreign table 401 */ 402 @VisibleForTesting 403 void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, 404 String foreignTable) { 405 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 406 foreignTable + ")", null); 407 if (count > 0) { 408 Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); 409 } 410 } 411 412 @VisibleForTesting 413 synchronized SQLiteDatabase getDatabase(Context context) { 414 // Always return the cached database, if we've got one 415 if (mDatabase != null) { 416 return mDatabase; 417 } 418 419 // Whenever we create or re-cache the databases, make sure that we haven't lost one 420 // to corruption 421 checkDatabases(); 422 423 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 424 mDatabase = helper.getWritableDatabase(); 425 DBHelper.BodyDatabaseHelper bodyHelper = 426 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 427 mBodyDatabase = bodyHelper.getWritableDatabase(); 428 if (mBodyDatabase != null) { 429 String bodyFileName = mBodyDatabase.getPath(); 430 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 431 } 432 433 // Restore accounts if the database is corrupted... 434 restoreIfNeeded(context, mDatabase); 435 // Check for any orphaned Messages in the updated/deleted tables 436 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 437 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 438 // Delete orphaned mailboxes/messages/policies (account no longer exists) 439 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, 440 Account.TABLE_NAME); 441 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, 442 Account.TABLE_NAME); 443 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, 444 Account.TABLE_NAME); 445 initUiProvider(); 446 preCacheData(); 447 return mDatabase; 448 } 449 450 /** 451 * Perform startup actions related to UI 452 */ 453 private void initUiProvider() { 454 // Clear mailbox sync status 455 mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + 456 "=" + UIProvider.SyncStatus.NO_SYNC); 457 } 458 459 /** 460 * Pre-cache all of the items in a given table meeting the selection criteria 461 * @param tableUri the table uri 462 * @param baseProjection the base projection of that table 463 * @param selection the selection criteria 464 */ 465 private void preCacheTable(Uri tableUri, String[] baseProjection, String selection) { 466 Cursor c = query(tableUri, EmailContent.ID_PROJECTION, selection, null, null); 467 try { 468 while (c.moveToNext()) { 469 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 470 Cursor cachedCursor = query(ContentUris.withAppendedId( 471 tableUri, id), baseProjection, null, null, null); 472 if (cachedCursor != null) { 473 // For accounts, create a mailbox type map entry (if necessary) 474 if (tableUri == Account.CONTENT_URI) { 475 getOrCreateAccountMailboxTypeMap(id); 476 } 477 cachedCursor.close(); 478 } 479 } 480 } finally { 481 c.close(); 482 } 483 } 484 485 private final HashMap<Long, HashMap<Integer, Long>> mMailboxTypeMap = 486 new HashMap<Long, HashMap<Integer, Long>>(); 487 488 private HashMap<Integer, Long> getOrCreateAccountMailboxTypeMap(long accountId) { 489 synchronized(mMailboxTypeMap) { 490 HashMap<Integer, Long> accountMailboxTypeMap = mMailboxTypeMap.get(accountId); 491 if (accountMailboxTypeMap == null) { 492 accountMailboxTypeMap = new HashMap<Integer, Long>(); 493 mMailboxTypeMap.put(accountId, accountMailboxTypeMap); 494 } 495 return accountMailboxTypeMap; 496 } 497 } 498 499 private void addToMailboxTypeMap(Cursor c) { 500 long accountId = c.getLong(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN); 501 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 502 synchronized(mMailboxTypeMap) { 503 HashMap<Integer, Long> accountMailboxTypeMap = 504 getOrCreateAccountMailboxTypeMap(accountId); 505 accountMailboxTypeMap.put(type, c.getLong(Mailbox.CONTENT_ID_COLUMN)); 506 } 507 } 508 509 private long getMailboxIdFromMailboxTypeMap(long accountId, int type) { 510 synchronized(mMailboxTypeMap) { 511 HashMap<Integer, Long> accountMap = mMailboxTypeMap.get(accountId); 512 Long mailboxId = null; 513 if (accountMap != null) { 514 mailboxId = accountMap.get(type); 515 } 516 if (mailboxId == null) return Mailbox.NO_MAILBOX; 517 return mailboxId; 518 } 519 } 520 521 private void preCacheData() { 522 synchronized(mMailboxTypeMap) { 523 mMailboxTypeMap.clear(); 524 525 // Pre-cache accounts, host auth's, policies, and special mailboxes 526 preCacheTable(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null); 527 preCacheTable(HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, null); 528 preCacheTable(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null); 529 preCacheTable(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 530 MAILBOX_PRE_CACHE_SELECTION); 531 532 // Create a map from account,type to a mailbox 533 Map<String, Cursor> snapshot = mCacheMailbox.getSnapshot(); 534 Collection<Cursor> values = snapshot.values(); 535 if (values != null) { 536 for (Cursor c: values) { 537 if (c.moveToFirst()) { 538 addToMailboxTypeMap(c); 539 } 540 } 541 } 542 } 543 } 544 545 /*package*/ static SQLiteDatabase getReadableDatabase(Context context) { 546 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 547 return helper.getReadableDatabase(); 548 } 549 550 /** 551 * Restore user Account and HostAuth data from our backup database 552 */ 553 public static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 554 if (MailActivityEmail.DEBUG) { 555 Log.w(TAG, "restoreIfNeeded..."); 556 } 557 // Check for legacy backup 558 String legacyBackup = Preferences.getLegacyBackupPreference(context); 559 // If there's a legacy backup, create a new-style backup and delete the legacy backup 560 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 561 // corrupt, oh well... 562 if (!TextUtils.isEmpty(legacyBackup)) { 563 backupAccounts(context, mainDatabase); 564 Preferences.clearLegacyBackupPreference(context); 565 Log.w(TAG, "Created new EmailProvider backup database"); 566 return; 567 } 568 569 // If we have accounts, we're done 570 Cursor c = mainDatabase.query(Account.TABLE_NAME, EmailContent.ID_PROJECTION, null, null, 571 null, null, null); 572 try { 573 if (c.moveToFirst()) { 574 if (MailActivityEmail.DEBUG) { 575 Log.w(TAG, "restoreIfNeeded: Account exists."); 576 } 577 return; // At least one account exists. 578 } 579 } finally { 580 c.close(); 581 } 582 583 restoreAccounts(context, mainDatabase); 584 } 585 586 /** {@inheritDoc} */ 587 @Override 588 public void shutdown() { 589 if (mDatabase != null) { 590 mDatabase.close(); 591 mDatabase = null; 592 } 593 if (mBodyDatabase != null) { 594 mBodyDatabase.close(); 595 mBodyDatabase = null; 596 } 597 } 598 599 /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 600 if (database != null) { 601 // We'll look at all of the items in the table; there won't be many typically 602 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 603 // Usually, there will be nothing in these tables, so make a quick check 604 try { 605 if (c.getCount() == 0) return; 606 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 607 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 608 ArrayList<Long> deleteList = new ArrayList<Long>(); 609 String[] bindArray = new String[1]; 610 while (c.moveToNext()) { 611 // Get the mailbox key and see if we've already found this mailbox 612 // If so, we're fine 613 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 614 // If we already know this mailbox doesn't exist, mark the message for deletion 615 if (notFoundMailboxes.contains(mailboxId)) { 616 deleteList.add(c.getLong(ORPHANS_ID)); 617 // If we don't know about this mailbox, we'll try to find it 618 } else if (!foundMailboxes.contains(mailboxId)) { 619 bindArray[0] = Long.toString(mailboxId); 620 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 621 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 622 try { 623 // If it exists, we'll add it to the "found" mailboxes 624 if (boxCursor.moveToFirst()) { 625 foundMailboxes.add(mailboxId); 626 // Otherwise, we'll add to "not found" and mark the message for deletion 627 } else { 628 notFoundMailboxes.add(mailboxId); 629 deleteList.add(c.getLong(ORPHANS_ID)); 630 } 631 } finally { 632 boxCursor.close(); 633 } 634 } 635 } 636 // Now, delete the orphan messages 637 for (long messageId: deleteList) { 638 bindArray[0] = Long.toString(messageId); 639 database.delete(tableName, WHERE_ID, bindArray); 640 } 641 } finally { 642 c.close(); 643 } 644 } 645 } 646 647 @Override 648 public int delete(Uri uri, String selection, String[] selectionArgs) { 649 final int match = findMatch(uri, "delete"); 650 Context context = getContext(); 651 // Pick the correct database for this operation 652 // If we're in a transaction already (which would happen during applyBatch), then the 653 // body database is already attached to the email database and any attempt to use the 654 // body database directly will result in a SQLiteException (the database is locked) 655 SQLiteDatabase db = getDatabase(context); 656 int table = match >> BASE_SHIFT; 657 String id = "0"; 658 boolean messageDeletion = false; 659 ContentResolver resolver = context.getContentResolver(); 660 661 ContentCache cache = mContentCaches[table]; 662 String tableName = TABLE_NAMES[table]; 663 int result = -1; 664 665 try { 666 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 667 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 668 notifyUIConversation(uri); 669 } 670 } 671 switch (match) { 672 case UI_MESSAGE: 673 return uiDeleteMessage(uri); 674 case UI_ACCOUNT_DATA: 675 return uiDeleteAccountData(uri); 676 case UI_ACCOUNT: 677 return uiDeleteAccount(uri); 678 case MESSAGE_SELECTION: 679 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 680 selectionArgs, null, null, null); 681 try { 682 if (findCursor.moveToFirst()) { 683 return delete(ContentUris.withAppendedId( 684 Message.CONTENT_URI, 685 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 686 null, null); 687 } else { 688 return 0; 689 } 690 } finally { 691 findCursor.close(); 692 } 693 // These are cases in which one or more Messages might get deleted, either by 694 // cascade or explicitly 695 case MAILBOX_ID: 696 case MAILBOX: 697 case ACCOUNT_ID: 698 case ACCOUNT: 699 case MESSAGE: 700 case SYNCED_MESSAGE_ID: 701 case MESSAGE_ID: 702 // Handle lost Body records here, since this cannot be done in a trigger 703 // The process is: 704 // 1) Begin a transaction, ensuring that both databases are affected atomically 705 // 2) Do the requested deletion, with cascading deletions handled in triggers 706 // 3) End the transaction, committing all changes atomically 707 // 708 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 709 messageDeletion = true; 710 db.beginTransaction(); 711 break; 712 } 713 switch (match) { 714 case BODY_ID: 715 case DELETED_MESSAGE_ID: 716 case SYNCED_MESSAGE_ID: 717 case MESSAGE_ID: 718 case UPDATED_MESSAGE_ID: 719 case ATTACHMENT_ID: 720 case MAILBOX_ID: 721 case ACCOUNT_ID: 722 case HOSTAUTH_ID: 723 case POLICY_ID: 724 case QUICK_RESPONSE_ID: 725 id = uri.getPathSegments().get(1); 726 if (match == SYNCED_MESSAGE_ID) { 727 // For synced messages, first copy the old message to the deleted table and 728 // delete it from the updated table (in case it was updated first) 729 // Note that this is all within a transaction, for atomicity 730 db.execSQL(DELETED_MESSAGE_INSERT + id); 731 db.execSQL(UPDATED_MESSAGE_DELETE + id); 732 } 733 if (cache != null) { 734 cache.lock(id); 735 } 736 try { 737 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 738 if (cache != null) { 739 switch(match) { 740 case ACCOUNT_ID: 741 // Account deletion will clear all of the caches, as HostAuth's, 742 // Mailboxes, and Messages will be deleted in the process 743 mCacheMailbox.invalidate("Delete", uri, selection); 744 mCacheHostAuth.invalidate("Delete", uri, selection); 745 mCachePolicy.invalidate("Delete", uri, selection); 746 //$FALL-THROUGH$ 747 case MAILBOX_ID: 748 // Mailbox deletion will clear the Message cache 749 mCacheMessage.invalidate("Delete", uri, selection); 750 //$FALL-THROUGH$ 751 case SYNCED_MESSAGE_ID: 752 case MESSAGE_ID: 753 case HOSTAUTH_ID: 754 case POLICY_ID: 755 cache.invalidate("Delete", uri, selection); 756 // Make sure all data is properly cached 757 if (match != MESSAGE_ID) { 758 preCacheData(); 759 } 760 break; 761 } 762 } 763 } finally { 764 if (cache != null) { 765 cache.unlock(id); 766 } 767 } 768 if (match == ACCOUNT_ID) { 769 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 770 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 771 } else if (match == MAILBOX_ID) { 772 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, id); 773 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, id); 774 } else if (match == ATTACHMENT_ID) { 775 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 776 } 777 break; 778 case ATTACHMENTS_MESSAGE_ID: 779 // All attachments for the given message 780 id = uri.getPathSegments().get(2); 781 result = db.delete(tableName, 782 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 783 break; 784 785 case BODY: 786 case MESSAGE: 787 case DELETED_MESSAGE: 788 case UPDATED_MESSAGE: 789 case ATTACHMENT: 790 case MAILBOX: 791 case ACCOUNT: 792 case HOSTAUTH: 793 case POLICY: 794 switch(match) { 795 // See the comments above for deletion of ACCOUNT_ID, etc 796 case ACCOUNT: 797 mCacheMailbox.invalidate("Delete", uri, selection); 798 mCacheHostAuth.invalidate("Delete", uri, selection); 799 mCachePolicy.invalidate("Delete", uri, selection); 800 //$FALL-THROUGH$ 801 case MAILBOX: 802 mCacheMessage.invalidate("Delete", uri, selection); 803 //$FALL-THROUGH$ 804 case MESSAGE: 805 case HOSTAUTH: 806 case POLICY: 807 cache.invalidate("Delete", uri, selection); 808 break; 809 } 810 result = db.delete(tableName, selection, selectionArgs); 811 switch(match) { 812 case ACCOUNT: 813 case MAILBOX: 814 case HOSTAUTH: 815 case POLICY: 816 // Make sure all data is properly cached 817 preCacheData(); 818 break; 819 } 820 break; 821 822 default: 823 throw new IllegalArgumentException("Unknown URI " + uri); 824 } 825 if (messageDeletion) { 826 if (match == MESSAGE_ID) { 827 // Delete the Body record associated with the deleted message 828 db.execSQL(DELETE_BODY + id); 829 } else { 830 // Delete any orphaned Body records 831 db.execSQL(DELETE_ORPHAN_BODIES); 832 } 833 db.setTransactionSuccessful(); 834 } 835 } catch (SQLiteException e) { 836 checkDatabases(); 837 throw e; 838 } finally { 839 if (messageDeletion) { 840 db.endTransaction(); 841 } 842 } 843 844 // Notify all notifier cursors 845 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 846 847 // Notify all email content cursors 848 resolver.notifyChange(EmailContent.CONTENT_URI, null); 849 return result; 850 } 851 852 @Override 853 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 854 public String getType(Uri uri) { 855 int match = findMatch(uri, "getType"); 856 switch (match) { 857 case BODY_ID: 858 return "vnd.android.cursor.item/email-body"; 859 case BODY: 860 return "vnd.android.cursor.dir/email-body"; 861 case UPDATED_MESSAGE_ID: 862 case MESSAGE_ID: 863 // NOTE: According to the framework folks, we're supposed to invent mime types as 864 // a way of passing information to drag & drop recipients. 865 // If there's a mailboxId parameter in the url, we respond with a mime type that 866 // has -n appended, where n is the mailboxId of the message. The drag & drop code 867 // uses this information to know not to allow dragging the item to its own mailbox 868 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 869 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 870 if (mailboxId != null) { 871 mimeType += "-" + mailboxId; 872 } 873 return mimeType; 874 case UPDATED_MESSAGE: 875 case MESSAGE: 876 return "vnd.android.cursor.dir/email-message"; 877 case MAILBOX: 878 return "vnd.android.cursor.dir/email-mailbox"; 879 case MAILBOX_ID: 880 return "vnd.android.cursor.item/email-mailbox"; 881 case ACCOUNT: 882 return "vnd.android.cursor.dir/email-account"; 883 case ACCOUNT_ID: 884 return "vnd.android.cursor.item/email-account"; 885 case ATTACHMENTS_MESSAGE_ID: 886 case ATTACHMENT: 887 return "vnd.android.cursor.dir/email-attachment"; 888 case ATTACHMENT_ID: 889 return EMAIL_ATTACHMENT_MIME_TYPE; 890 case HOSTAUTH: 891 return "vnd.android.cursor.dir/email-hostauth"; 892 case HOSTAUTH_ID: 893 return "vnd.android.cursor.item/email-hostauth"; 894 default: 895 throw new IllegalArgumentException("Unknown URI " + uri); 896 } 897 } 898 899 private static final Uri UIPROVIDER_CONVERSATION_NOTIFIER = 900 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessages"); 901 private static final Uri UIPROVIDER_FOLDER_NOTIFIER = 902 Uri.parse("content://" + UIProvider.AUTHORITY + "/uifolder"); 903 private static final Uri UIPROVIDER_FOLDERLIST_NOTIFIER = 904 Uri.parse("content://" + UIProvider.AUTHORITY + "/uifolders"); 905 private static final Uri UIPROVIDER_ACCOUNT_NOTIFIER = 906 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiaccount"); 907 public static final Uri UIPROVIDER_SETTINGS_NOTIFIER = 908 Uri.parse("content://" + UIProvider.AUTHORITY + "/uisettings"); 909 private static final Uri UIPROVIDER_ATTACHMENT_NOTIFIER = 910 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachment"); 911 private static final Uri UIPROVIDER_ATTACHMENTS_NOTIFIER = 912 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiattachments"); 913 public static final Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER = 914 Uri.parse("content://" + UIProvider.AUTHORITY + "/uiaccts"); 915 private static final Uri UIPROVIDER_MESSAGE_NOTIFIER = 916 Uri.parse("content://" + UIProvider.AUTHORITY + "/uimessage"); 917 private static final Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER = 918 Uri.parse("content://" + UIProvider.AUTHORITY + "/uirecentfolders"); 919 920 @Override 921 public Uri insert(Uri uri, ContentValues values) { 922 int match = findMatch(uri, "insert"); 923 Context context = getContext(); 924 ContentResolver resolver = context.getContentResolver(); 925 926 // See the comment at delete(), above 927 SQLiteDatabase db = getDatabase(context); 928 int table = match >> BASE_SHIFT; 929 String id = "0"; 930 long longId; 931 932 // We do NOT allow setting of unreadCount/messageCount via the provider 933 // These columns are maintained via triggers 934 if (match == MAILBOX_ID || match == MAILBOX) { 935 values.put(MailboxColumns.UNREAD_COUNT, 0); 936 values.put(MailboxColumns.MESSAGE_COUNT, 0); 937 } 938 939 Uri resultUri = null; 940 941 try { 942 switch (match) { 943 case UI_SAVEDRAFT: 944 return uiSaveDraft(uri, values); 945 case UI_SENDMAIL: 946 return uiSendMail(uri, values); 947 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 948 // or DELETED_MESSAGE; see the comment below for details 949 case UPDATED_MESSAGE: 950 case DELETED_MESSAGE: 951 case MESSAGE: 952 case BODY: 953 case ATTACHMENT: 954 case MAILBOX: 955 case ACCOUNT: 956 case HOSTAUTH: 957 case POLICY: 958 case QUICK_RESPONSE: 959 longId = db.insert(TABLE_NAMES[table], "foo", values); 960 resultUri = ContentUris.withAppendedId(uri, longId); 961 switch(match) { 962 case MESSAGE: 963 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 964 notifyUIConversationMailbox(values.getAsLong(Message.MAILBOX_KEY)); 965 } 966 break; 967 case MAILBOX: 968 if (values.containsKey(MailboxColumns.TYPE)) { 969 // Only cache special mailbox types 970 int type = values.getAsInteger(MailboxColumns.TYPE); 971 if (type != Mailbox.TYPE_INBOX && type != Mailbox.TYPE_OUTBOX && 972 type != Mailbox.TYPE_DRAFTS && type != Mailbox.TYPE_SENT && 973 type != Mailbox.TYPE_TRASH && type != Mailbox.TYPE_SEARCH) { 974 break; 975 } 976 } 977 // Notify the account when a new mailbox is added 978 Long accountId = values.getAsLong(MailboxColumns.ACCOUNT_KEY); 979 if (accountId != null && accountId.longValue() > 0) { 980 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); 981 } 982 //$FALL-THROUGH$ 983 case ACCOUNT: 984 case HOSTAUTH: 985 case POLICY: 986 // Cache new account, host auth, policy, and some mailbox rows 987 Cursor c = query(resultUri, CACHE_PROJECTIONS[table], null, null, null); 988 if (c != null) { 989 if (match == MAILBOX) { 990 addToMailboxTypeMap(c); 991 } else if (match == ACCOUNT) { 992 getOrCreateAccountMailboxTypeMap(longId); 993 } 994 c.close(); 995 } 996 break; 997 } 998 // Clients shouldn't normally be adding rows to these tables, as they are 999 // maintained by triggers. However, we need to be able to do this for unit 1000 // testing, so we allow the insert and then throw the same exception that we 1001 // would if this weren't allowed. 1002 if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) { 1003 throw new IllegalArgumentException("Unknown URL " + uri); 1004 } else if (match == ATTACHMENT) { 1005 int flags = 0; 1006 if (values.containsKey(Attachment.FLAGS)) { 1007 flags = values.getAsInteger(Attachment.FLAGS); 1008 } 1009 // Report all new attachments to the download service 1010 mAttachmentService.attachmentChanged(getContext(), longId, flags); 1011 } else if (match == ACCOUNT) { 1012 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 1013 } 1014 break; 1015 case MAILBOX_ID: 1016 // This implies adding a message to a mailbox 1017 // Hmm, a problem here is that we can't link the account as well, so it must be 1018 // already in the values... 1019 longId = Long.parseLong(uri.getPathSegments().get(1)); 1020 values.put(MessageColumns.MAILBOX_KEY, longId); 1021 return insert(Message.CONTENT_URI, values); // Recurse 1022 case MESSAGE_ID: 1023 // This implies adding an attachment to a message. 1024 id = uri.getPathSegments().get(1); 1025 longId = Long.parseLong(id); 1026 values.put(AttachmentColumns.MESSAGE_KEY, longId); 1027 return insert(Attachment.CONTENT_URI, values); // Recurse 1028 case ACCOUNT_ID: 1029 // This implies adding a mailbox to an account. 1030 longId = Long.parseLong(uri.getPathSegments().get(1)); 1031 values.put(MailboxColumns.ACCOUNT_KEY, longId); 1032 return insert(Mailbox.CONTENT_URI, values); // Recurse 1033 case ATTACHMENTS_MESSAGE_ID: 1034 longId = db.insert(TABLE_NAMES[table], "foo", values); 1035 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 1036 break; 1037 default: 1038 throw new IllegalArgumentException("Unknown URL " + uri); 1039 } 1040 } catch (SQLiteException e) { 1041 checkDatabases(); 1042 throw e; 1043 } 1044 1045 // Notify all notifier cursors 1046 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 1047 1048 // Notify all existing cursors. 1049 resolver.notifyChange(EmailContent.CONTENT_URI, null); 1050 return resultUri; 1051 } 1052 1053 @Override 1054 public boolean onCreate() { 1055 Context context = getContext(); 1056 EmailContent.init(context); 1057 if (INTEGRITY_CHECK_URI == null) { 1058 INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + 1059 "/integrityCheck"); 1060 ACCOUNT_BACKUP_URI = 1061 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 1062 FOLDER_STATUS_URI = 1063 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 1064 FOLDER_REFRESH_URI = 1065 Uri.parse("content://" + EmailContent.AUTHORITY + "/refresh"); 1066 EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); 1067 } 1068 checkDatabases(); 1069 if (sURIMatcher == null) { 1070 sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 1071 // Email URI matching table 1072 UriMatcher matcher = sURIMatcher; 1073 1074 // All accounts 1075 matcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 1076 // A specific account 1077 // insert into this URI causes a mailbox to be added to the account 1078 matcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 1079 matcher.addURI(EmailContent.AUTHORITY, "account/default", ACCOUNT_DEFAULT_ID); 1080 matcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); 1081 1082 // Special URI to reset the new message count. Only update works, and values 1083 // will be ignored. 1084 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 1085 ACCOUNT_RESET_NEW_COUNT); 1086 matcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 1087 ACCOUNT_RESET_NEW_COUNT_ID); 1088 1089 // All mailboxes 1090 matcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 1091 // A specific mailbox 1092 // insert into this URI causes a message to be added to the mailbox 1093 // ** NOTE For now, the accountKey must be set manually in the values! 1094 matcher.addURI(EmailContent.AUTHORITY, "mailbox/#", MAILBOX_ID); 1095 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdFromAccountAndType/#/#", 1096 MAILBOX_ID_FROM_ACCOUNT_AND_TYPE); 1097 matcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", 1098 MAILBOX_NOTIFICATION); 1099 matcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 1100 MAILBOX_MOST_RECENT_MESSAGE); 1101 1102 // All messages 1103 matcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 1104 // A specific message 1105 // insert into this URI causes an attachment to be added to the message 1106 matcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 1107 1108 // A specific attachment 1109 matcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 1110 // A specific attachment (the header information) 1111 matcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 1112 // The attachments of a specific message (query only) (insert & delete TBD) 1113 matcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 1114 ATTACHMENTS_MESSAGE_ID); 1115 1116 // All mail bodies 1117 matcher.addURI(EmailContent.AUTHORITY, "body", BODY); 1118 // A specific mail body 1119 matcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 1120 1121 // All hostauth records 1122 matcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 1123 // A specific hostauth 1124 matcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); 1125 1126 // Atomically a constant value to a particular field of a mailbox/account 1127 matcher.addURI(EmailContent.AUTHORITY, "mailboxIdAddToField/#", 1128 MAILBOX_ID_ADD_TO_FIELD); 1129 matcher.addURI(EmailContent.AUTHORITY, "accountIdAddToField/#", 1130 ACCOUNT_ID_ADD_TO_FIELD); 1131 1132 /** 1133 * THIS URI HAS SPECIAL SEMANTICS 1134 * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK 1135 * TO A SERVER VIA A SYNC ADAPTER 1136 */ 1137 matcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 1138 matcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); 1139 1140 /** 1141 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 1142 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 1143 * BY THE UI APPLICATION 1144 */ 1145 // All deleted messages 1146 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 1147 // A specific deleted message 1148 matcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 1149 1150 // All updated messages 1151 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 1152 // A specific updated message 1153 matcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 1154 1155 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 1156 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 1157 1158 matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 1159 matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 1160 1161 // All quick responses 1162 matcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 1163 // A specific quick response 1164 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 1165 // All quick responses associated with a particular account id 1166 matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 1167 QUICK_RESPONSE_ACCOUNT_ID); 1168 1169 matcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 1170 matcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); 1171 matcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 1172 matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 1173 matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 1174 matcher.addURI(EmailContent.AUTHORITY, "uisendmail/#", UI_SENDMAIL); 1175 matcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 1176 matcher.addURI(EmailContent.AUTHORITY, "uisavedraft/#", UI_SAVEDRAFT); 1177 matcher.addURI(EmailContent.AUTHORITY, "uiupdatedraft/#", UI_UPDATEDRAFT); 1178 matcher.addURI(EmailContent.AUTHORITY, "uisenddraft/#", UI_SENDDRAFT); 1179 matcher.addURI(EmailContent.AUTHORITY, "uirefresh/#", UI_FOLDER_REFRESH); 1180 matcher.addURI(EmailContent.AUTHORITY, "uifolder/#", UI_FOLDER); 1181 matcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 1182 matcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 1183 matcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 1184 matcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 1185 matcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 1186 matcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 1187 matcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 1188 matcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 1189 matcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 1190 matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", 1191 UI_DEFAULT_RECENT_FOLDERS); 1192 matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", 1193 ACCOUNT_PICK_TRASH_FOLDER); 1194 matcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", 1195 ACCOUNT_PICK_SENT_FOLDER); 1196 1197 // Do this last, so that EmailContent/EmailProvider are initialized 1198 MailActivityEmail.setServicesEnabledAsync(context); 1199 } 1200 return false; 1201 } 1202 1203 /** 1204 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1205 * always be in sync (i.e. there are two database or NO databases). This code will delete 1206 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1207 * will exist after either of the individual databases is deleted due to data corruption. 1208 */ 1209 public synchronized void checkDatabases() { 1210 // Uncache the databases 1211 if (mDatabase != null) { 1212 mDatabase = null; 1213 } 1214 if (mBodyDatabase != null) { 1215 mBodyDatabase = null; 1216 } 1217 // Look for orphans, and delete as necessary; these must always be in sync 1218 File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1219 File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1220 1221 // TODO Make sure attachments are deleted 1222 if (databaseFile.exists() && !bodyFile.exists()) { 1223 Log.w(TAG, "Deleting orphaned EmailProvider database..."); 1224 databaseFile.delete(); 1225 } else if (bodyFile.exists() && !databaseFile.exists()) { 1226 Log.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1227 bodyFile.delete(); 1228 } 1229 } 1230 @Override 1231 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1232 String sortOrder) { 1233 long time = 0L; 1234 if (MailActivityEmail.DEBUG) { 1235 time = System.nanoTime(); 1236 } 1237 Cursor c = null; 1238 int match; 1239 try { 1240 match = findMatch(uri, "query"); 1241 } catch (IllegalArgumentException e) { 1242 String uriString = uri.toString(); 1243 // If we were passed an illegal uri, see if it ends in /-1 1244 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1245 if (uriString != null && uriString.endsWith("/-1")) { 1246 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1247 match = findMatch(uri, "query"); 1248 switch (match) { 1249 case BODY_ID: 1250 case MESSAGE_ID: 1251 case DELETED_MESSAGE_ID: 1252 case UPDATED_MESSAGE_ID: 1253 case ATTACHMENT_ID: 1254 case MAILBOX_ID: 1255 case ACCOUNT_ID: 1256 case HOSTAUTH_ID: 1257 case POLICY_ID: 1258 return new MatrixCursorWithCachedColumns(projection, 0); 1259 } 1260 } 1261 throw e; 1262 } 1263 Context context = getContext(); 1264 // See the comment at delete(), above 1265 SQLiteDatabase db = getDatabase(context); 1266 int table = match >> BASE_SHIFT; 1267 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1268 String id; 1269 1270 // Find the cache for this query's table (if any) 1271 ContentCache cache = null; 1272 String tableName = TABLE_NAMES[table]; 1273 // We can only use the cache if there's no selection 1274 if (selection == null) { 1275 cache = mContentCaches[table]; 1276 } 1277 if (cache == null) { 1278 ContentCache.notCacheable(uri, selection); 1279 } 1280 1281 try { 1282 switch (match) { 1283 // First, dispatch queries from UnfiedEmail 1284 case UI_SEARCH: 1285 c = uiSearch(uri, projection); 1286 return c; 1287 case UI_ACCTS: 1288 c = uiAccounts(projection); 1289 return c; 1290 case UI_UNDO: 1291 return uiUndo(projection); 1292 case UI_SUBFOLDERS: 1293 case UI_MESSAGES: 1294 case UI_MESSAGE: 1295 case UI_FOLDER: 1296 case UI_ACCOUNT: 1297 case UI_ATTACHMENT: 1298 case UI_ATTACHMENTS: 1299 case UI_CONVERSATION: 1300 case UI_RECENT_FOLDERS: 1301 case UI_ALL_FOLDERS: 1302 // For now, we don't allow selection criteria within these queries 1303 if (selection != null || selectionArgs != null) { 1304 throw new IllegalArgumentException("UI queries can't have selection/args"); 1305 } 1306 c = uiQuery(match, uri, projection); 1307 return c; 1308 case UI_FOLDERS: 1309 c = uiFolders(uri, projection); 1310 return c; 1311 case UI_FOLDER_LOAD_MORE: 1312 c = uiFolderLoadMore(uri); 1313 return c; 1314 case UI_FOLDER_REFRESH: 1315 c = uiFolderRefresh(uri); 1316 return c; 1317 case MAILBOX_NOTIFICATION: 1318 c = notificationQuery(uri); 1319 return c; 1320 case MAILBOX_MOST_RECENT_MESSAGE: 1321 c = mostRecentMessageQuery(uri); 1322 return c; 1323 case ACCOUNT_DEFAULT_ID: 1324 // Start with a snapshot of the cache 1325 Map<String, Cursor> accountCache = mCacheAccount.getSnapshot(); 1326 long accountId = Account.NO_ACCOUNT; 1327 // Find the account with "isDefault" set, or the lowest account ID otherwise. 1328 // Note that the snapshot from the cached isn't guaranteed to be sorted in any 1329 // way. 1330 Collection<Cursor> accounts = accountCache.values(); 1331 for (Cursor accountCursor: accounts) { 1332 // For now, at least, we can have zero count cursors (e.g. if someone looks 1333 // up a non-existent id); we need to skip these 1334 if (accountCursor.moveToFirst()) { 1335 boolean isDefault = 1336 accountCursor.getInt(Account.CONTENT_IS_DEFAULT_COLUMN) == 1; 1337 long iterId = accountCursor.getLong(Account.CONTENT_ID_COLUMN); 1338 // We'll remember this one if it's the default or the first one we see 1339 if (isDefault) { 1340 accountId = iterId; 1341 break; 1342 } else if ((accountId == Account.NO_ACCOUNT) || (iterId < accountId)) { 1343 accountId = iterId; 1344 } 1345 } 1346 } 1347 // Return a cursor with an id projection 1348 MatrixCursor mc = new MatrixCursorWithCachedColumns(EmailContent.ID_PROJECTION); 1349 mc.addRow(new Object[] {accountId}); 1350 c = mc; 1351 break; 1352 case MAILBOX_ID_FROM_ACCOUNT_AND_TYPE: 1353 // Get accountId and type and find the mailbox in our map 1354 List<String> pathSegments = uri.getPathSegments(); 1355 accountId = Long.parseLong(pathSegments.get(1)); 1356 int type = Integer.parseInt(pathSegments.get(2)); 1357 long mailboxId = getMailboxIdFromMailboxTypeMap(accountId, type); 1358 // Return a cursor with an id projection 1359 mc = new MatrixCursorWithCachedColumns(EmailContent.ID_PROJECTION); 1360 mc.addRow(new Object[] {mailboxId}); 1361 c = mc; 1362 break; 1363 case BODY: 1364 case MESSAGE: 1365 case UPDATED_MESSAGE: 1366 case DELETED_MESSAGE: 1367 case ATTACHMENT: 1368 case MAILBOX: 1369 case ACCOUNT: 1370 case HOSTAUTH: 1371 case POLICY: 1372 case QUICK_RESPONSE: 1373 // Special-case "count of accounts"; it's common and we always know it 1374 if (match == ACCOUNT && Arrays.equals(projection, EmailContent.COUNT_COLUMNS) && 1375 selection == null && limit.equals("1")) { 1376 int accountCount = mMailboxTypeMap.size(); 1377 // In the rare case there are MAX_CACHED_ACCOUNTS or more, we can't do this 1378 if (accountCount < MAX_CACHED_ACCOUNTS) { 1379 mc = new MatrixCursorWithCachedColumns(projection, 1); 1380 mc.addRow(new Object[] {accountCount}); 1381 c = mc; 1382 break; 1383 } 1384 } 1385 c = db.query(tableName, projection, 1386 selection, selectionArgs, null, null, sortOrder, limit); 1387 break; 1388 case BODY_ID: 1389 case MESSAGE_ID: 1390 case DELETED_MESSAGE_ID: 1391 case UPDATED_MESSAGE_ID: 1392 case ATTACHMENT_ID: 1393 case MAILBOX_ID: 1394 case ACCOUNT_ID: 1395 case HOSTAUTH_ID: 1396 case POLICY_ID: 1397 case QUICK_RESPONSE_ID: 1398 id = uri.getPathSegments().get(1); 1399 if (cache != null) { 1400 c = cache.getCachedCursor(id, projection); 1401 } 1402 if (c == null) { 1403 CacheToken token = null; 1404 if (cache != null) { 1405 token = cache.getCacheToken(id); 1406 } 1407 c = db.query(tableName, projection, whereWithId(id, selection), 1408 selectionArgs, null, null, sortOrder, limit); 1409 if (cache != null) { 1410 c = cache.putCursor(c, id, projection, token); 1411 } 1412 } 1413 break; 1414 case ATTACHMENTS_MESSAGE_ID: 1415 // All attachments for the given message 1416 id = uri.getPathSegments().get(2); 1417 c = db.query(Attachment.TABLE_NAME, projection, 1418 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1419 selectionArgs, null, null, sortOrder, limit); 1420 break; 1421 case QUICK_RESPONSE_ACCOUNT_ID: 1422 // All quick responses for the given account 1423 id = uri.getPathSegments().get(2); 1424 c = db.query(QuickResponse.TABLE_NAME, projection, 1425 whereWith(QuickResponse.ACCOUNT_KEY + "=" + id, selection), 1426 selectionArgs, null, null, sortOrder); 1427 break; 1428 default: 1429 throw new IllegalArgumentException("Unknown URI " + uri); 1430 } 1431 } catch (SQLiteException e) { 1432 checkDatabases(); 1433 throw e; 1434 } catch (RuntimeException e) { 1435 checkDatabases(); 1436 e.printStackTrace(); 1437 throw e; 1438 } finally { 1439 if (cache != null && c != null && MailActivityEmail.DEBUG) { 1440 cache.recordQueryTime(c, System.nanoTime() - time); 1441 } 1442 if (c == null) { 1443 // This should never happen, but let's be sure to log it... 1444 Log.e(TAG, "Query returning null for uri: " + uri + ", selection: " + selection); 1445 } 1446 } 1447 1448 if ((c != null) && !isTemporary()) { 1449 c.setNotificationUri(getContext().getContentResolver(), uri); 1450 } 1451 return c; 1452 } 1453 1454 private String whereWithId(String id, String selection) { 1455 StringBuilder sb = new StringBuilder(256); 1456 sb.append("_id="); 1457 sb.append(id); 1458 if (selection != null) { 1459 sb.append(" AND ("); 1460 sb.append(selection); 1461 sb.append(')'); 1462 } 1463 return sb.toString(); 1464 } 1465 1466 /** 1467 * Combine a locally-generated selection with a user-provided selection 1468 * 1469 * This introduces risk that the local selection might insert incorrect chars 1470 * into the SQL, so use caution. 1471 * 1472 * @param where locally-generated selection, must not be null 1473 * @param selection user-provided selection, may be null 1474 * @return a single selection string 1475 */ 1476 private String whereWith(String where, String selection) { 1477 if (selection == null) { 1478 return where; 1479 } 1480 StringBuilder sb = new StringBuilder(where); 1481 sb.append(" AND ("); 1482 sb.append(selection); 1483 sb.append(')'); 1484 1485 return sb.toString(); 1486 } 1487 1488 /** 1489 * Restore a HostAuth from a database, given its unique id 1490 * @param db the database 1491 * @param id the unique id (_id) of the row 1492 * @return a fully populated HostAuth or null if the row does not exist 1493 */ 1494 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1495 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1496 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1497 try { 1498 if (c.moveToFirst()) { 1499 HostAuth hostAuth = new HostAuth(); 1500 hostAuth.restore(c); 1501 return hostAuth; 1502 } 1503 return null; 1504 } finally { 1505 c.close(); 1506 } 1507 } 1508 1509 /** 1510 * Copy the Account and HostAuth tables from one database to another 1511 * @param fromDatabase the source database 1512 * @param toDatabase the destination database 1513 * @return the number of accounts copied, or -1 if an error occurred 1514 */ 1515 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1516 if (fromDatabase == null || toDatabase == null) return -1; 1517 1518 // Lock both databases; for the "from" database, we don't want anyone changing it from 1519 // under us; for the "to" database, we want to make the operation atomic 1520 int copyCount = 0; 1521 fromDatabase.beginTransaction(); 1522 try { 1523 toDatabase.beginTransaction(); 1524 try { 1525 // Delete anything hanging around here 1526 toDatabase.delete(Account.TABLE_NAME, null, null); 1527 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1528 1529 // Get our account cursor 1530 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1531 null, null, null, null, null); 1532 if (c == null) return 0; 1533 Log.d(TAG, "fromDatabase accounts: " + c.getCount()); 1534 try { 1535 // Loop through accounts, copying them and associated host auth's 1536 while (c.moveToNext()) { 1537 Account account = new Account(); 1538 account.restore(c); 1539 1540 // Clear security sync key and sync key, as these were specific to the 1541 // state of the account, and we've reset that... 1542 // Clear policy key so that we can re-establish policies from the server 1543 // TODO This is pretty EAS specific, but there's a lot of that around 1544 account.mSecuritySyncKey = null; 1545 account.mSyncKey = null; 1546 account.mPolicyKey = 0; 1547 1548 // Copy host auth's and update foreign keys 1549 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1550 account.mHostAuthKeyRecv); 1551 1552 // The account might have gone away, though very unlikely 1553 if (hostAuth == null) continue; 1554 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1555 hostAuth.toContentValues()); 1556 1557 // EAS accounts have no send HostAuth 1558 if (account.mHostAuthKeySend > 0) { 1559 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1560 // Belt and suspenders; I can't imagine that this is possible, 1561 // since we checked the validity of the account above, and the 1562 // database is now locked 1563 if (hostAuth == null) continue; 1564 account.mHostAuthKeySend = toDatabase.insert( 1565 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1566 } 1567 1568 // Now, create the account in the "to" database 1569 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1570 copyCount++; 1571 } 1572 } finally { 1573 c.close(); 1574 } 1575 1576 // Say it's ok to commit 1577 toDatabase.setTransactionSuccessful(); 1578 } finally { 1579 // STOPSHIP: Remove logging here and in at endTransaction() below 1580 Log.d(TAG, "ending toDatabase transaction; copyCount = " + copyCount); 1581 toDatabase.endTransaction(); 1582 } 1583 } catch (SQLiteException ex) { 1584 Log.w(TAG, "Exception while copying account tables", ex); 1585 copyCount = -1; 1586 } finally { 1587 Log.d(TAG, "ending fromDatabase transaction; copyCount = " + copyCount); 1588 fromDatabase.endTransaction(); 1589 } 1590 return copyCount; 1591 } 1592 1593 private static SQLiteDatabase getBackupDatabase(Context context) { 1594 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1595 return helper.getWritableDatabase(); 1596 } 1597 1598 /** 1599 * Backup account data, returning the number of accounts backed up 1600 */ 1601 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1602 if (MailActivityEmail.DEBUG) { 1603 Log.d(TAG, "backupAccounts..."); 1604 } 1605 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1606 try { 1607 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1608 if (numBackedUp < 0) { 1609 Log.e(TAG, "Account backup failed!"); 1610 } else if (MailActivityEmail.DEBUG) { 1611 Log.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1612 } 1613 return numBackedUp; 1614 } finally { 1615 if (backupDatabase != null) { 1616 backupDatabase.close(); 1617 } 1618 } 1619 } 1620 1621 /** 1622 * Restore account data, returning the number of accounts restored 1623 */ 1624 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1625 if (MailActivityEmail.DEBUG) { 1626 Log.d(TAG, "restoreAccounts..."); 1627 } 1628 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1629 try { 1630 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1631 if (numRecovered > 0) { 1632 Log.e(TAG, "Recovered " + numRecovered + " accounts!"); 1633 } else if (numRecovered < 0) { 1634 Log.e(TAG, "Account recovery failed?"); 1635 } else if (MailActivityEmail.DEBUG) { 1636 Log.d(TAG, "No accounts to restore..."); 1637 } 1638 return numRecovered; 1639 } finally { 1640 if (backupDatabase != null) { 1641 backupDatabase.close(); 1642 } 1643 } 1644 } 1645 1646 // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? 1647 // group by serverId) where dupes > 1; 1648 private static final String ACCOUNT_INTEGRITY_SQL = 1649 "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + 1650 " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; 1651 1652 @Override 1653 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1654 // Handle this special case the fastest possible way 1655 if (uri == INTEGRITY_CHECK_URI) { 1656 checkDatabases(); 1657 return 0; 1658 } else if (uri == ACCOUNT_BACKUP_URI) { 1659 return backupAccounts(getContext(), getDatabase(getContext())); 1660 } 1661 1662 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1663 Uri notificationUri = EmailContent.CONTENT_URI; 1664 1665 int match = findMatch(uri, "update"); 1666 Context context = getContext(); 1667 ContentResolver resolver = context.getContentResolver(); 1668 // See the comment at delete(), above 1669 SQLiteDatabase db = getDatabase(context); 1670 int table = match >> BASE_SHIFT; 1671 int result; 1672 1673 // We do NOT allow setting of unreadCount/messageCount via the provider 1674 // These columns are maintained via triggers 1675 if (match == MAILBOX_ID || match == MAILBOX) { 1676 values.remove(MailboxColumns.UNREAD_COUNT); 1677 values.remove(MailboxColumns.MESSAGE_COUNT); 1678 } 1679 1680 ContentCache cache = mContentCaches[table]; 1681 String tableName = TABLE_NAMES[table]; 1682 String id = "0"; 1683 1684 try { 1685outer: 1686 switch (match) { 1687 case ACCOUNT_PICK_TRASH_FOLDER: 1688 return pickTrashFolder(uri); 1689 case ACCOUNT_PICK_SENT_FOLDER: 1690 return pickSentFolder(uri); 1691 case UI_FOLDER: 1692 return uiUpdateFolder(uri, values); 1693 case UI_RECENT_FOLDERS: 1694 return uiUpdateRecentFolders(uri, values); 1695 case UI_DEFAULT_RECENT_FOLDERS: 1696 return uiPopulateRecentFolders(uri); 1697 case UI_ATTACHMENT: 1698 return uiUpdateAttachment(uri, values); 1699 case UI_UPDATEDRAFT: 1700 return uiUpdateDraft(uri, values); 1701 case UI_SENDDRAFT: 1702 return uiSendDraft(uri, values); 1703 case UI_MESSAGE: 1704 return uiUpdateMessage(uri, values); 1705 case ACCOUNT_CHECK: 1706 id = uri.getLastPathSegment(); 1707 // With any error, return 1 (a failure) 1708 int res = 1; 1709 Cursor ic = null; 1710 try { 1711 ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); 1712 if (ic.moveToFirst()) { 1713 res = ic.getInt(0); 1714 } 1715 } finally { 1716 if (ic != null) { 1717 ic.close(); 1718 } 1719 } 1720 // Count of duplicated mailboxes 1721 return res; 1722 case MAILBOX_ID_ADD_TO_FIELD: 1723 case ACCOUNT_ID_ADD_TO_FIELD: 1724 id = uri.getPathSegments().get(1); 1725 String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME); 1726 Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME); 1727 if (field == null || add == null) { 1728 throw new IllegalArgumentException("No field/add specified " + uri); 1729 } 1730 ContentValues actualValues = new ContentValues(); 1731 if (cache != null) { 1732 cache.lock(id); 1733 } 1734 try { 1735 db.beginTransaction(); 1736 try { 1737 Cursor c = db.query(tableName, 1738 new String[] {EmailContent.RECORD_ID, field}, 1739 whereWithId(id, selection), 1740 selectionArgs, null, null, null); 1741 try { 1742 result = 0; 1743 String[] bind = new String[1]; 1744 if (c.moveToNext()) { 1745 bind[0] = c.getString(0); // _id 1746 long value = c.getLong(1) + add; 1747 actualValues.put(field, value); 1748 result = db.update(tableName, actualValues, ID_EQUALS, bind); 1749 } 1750 db.setTransactionSuccessful(); 1751 } finally { 1752 c.close(); 1753 } 1754 } finally { 1755 db.endTransaction(); 1756 } 1757 } finally { 1758 if (cache != null) { 1759 cache.unlock(id, actualValues); 1760 } 1761 } 1762 break; 1763 case MESSAGE_SELECTION: 1764 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 1765 selectionArgs, null, null, null); 1766 try { 1767 if (findCursor.moveToFirst()) { 1768 return update(ContentUris.withAppendedId( 1769 Message.CONTENT_URI, 1770 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 1771 values, null, null); 1772 } else { 1773 return 0; 1774 } 1775 } finally { 1776 findCursor.close(); 1777 } 1778 case SYNCED_MESSAGE_ID: 1779 case UPDATED_MESSAGE_ID: 1780 case MESSAGE_ID: 1781 case BODY_ID: 1782 case ATTACHMENT_ID: 1783 case MAILBOX_ID: 1784 case ACCOUNT_ID: 1785 case HOSTAUTH_ID: 1786 case QUICK_RESPONSE_ID: 1787 case POLICY_ID: 1788 id = uri.getPathSegments().get(1); 1789 if (cache != null) { 1790 cache.lock(id); 1791 } 1792 try { 1793 if (match == SYNCED_MESSAGE_ID) { 1794 // For synced messages, first copy the old message to the updated table 1795 // Note the insert or ignore semantics, guaranteeing that only the first 1796 // update will be reflected in the updated message table; therefore this 1797 // row will always have the "original" data 1798 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1799 } else if (match == MESSAGE_ID) { 1800 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1801 } 1802 result = db.update(tableName, values, whereWithId(id, selection), 1803 selectionArgs); 1804 } catch (SQLiteException e) { 1805 // Null out values (so they aren't cached) and re-throw 1806 values = null; 1807 throw e; 1808 } finally { 1809 if (cache != null) { 1810 cache.unlock(id, values); 1811 } 1812 } 1813 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1814 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 1815 notifyUIConversation(uri); 1816 } 1817 } else if (match == ATTACHMENT_ID) { 1818 long attId = Integer.parseInt(id); 1819 if (values.containsKey(Attachment.FLAGS)) { 1820 int flags = values.getAsInteger(Attachment.FLAGS); 1821 mAttachmentService.attachmentChanged(context, attId, flags); 1822 } 1823 // Notify UI if necessary; there are only two columns we can change that 1824 // would be worth a notification 1825 if (values.containsKey(AttachmentColumns.UI_STATE) || 1826 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { 1827 // Notify on individual attachment 1828 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 1829 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 1830 if (att != null) { 1831 // And on owning Message 1832 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); 1833 } 1834 } 1835 } else if (match == MAILBOX_ID && values.containsKey(Mailbox.UI_SYNC_STATUS)) { 1836 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, id); 1837 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, id); 1838 } else if (match == ACCOUNT_ID) { 1839 // Notify individual account and "all accounts" 1840 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 1841 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 1842 } 1843 break; 1844 case BODY: 1845 case MESSAGE: 1846 case UPDATED_MESSAGE: 1847 case ATTACHMENT: 1848 case MAILBOX: 1849 case ACCOUNT: 1850 case HOSTAUTH: 1851 case POLICY: 1852 switch(match) { 1853 // To avoid invalidating the cache on updates, we execute them one at a 1854 // time using the XXX_ID uri; these are all executed atomically 1855 case ACCOUNT: 1856 case MAILBOX: 1857 case HOSTAUTH: 1858 case POLICY: 1859 Cursor c = db.query(tableName, EmailContent.ID_PROJECTION, 1860 selection, selectionArgs, null, null, null); 1861 db.beginTransaction(); 1862 result = 0; 1863 try { 1864 while (c.moveToNext()) { 1865 update(ContentUris.withAppendedId( 1866 uri, c.getLong(EmailContent.ID_PROJECTION_COLUMN)), 1867 values, null, null); 1868 result++; 1869 } 1870 db.setTransactionSuccessful(); 1871 } finally { 1872 db.endTransaction(); 1873 c.close(); 1874 } 1875 break outer; 1876 // Any cached table other than those above should be invalidated here 1877 case MESSAGE: 1878 // If we're doing some generic update, the whole cache needs to be 1879 // invalidated. This case should be quite rare 1880 cache.invalidate("Update", uri, selection); 1881 //$FALL-THROUGH$ 1882 default: 1883 result = db.update(tableName, values, selection, selectionArgs); 1884 break outer; 1885 } 1886 case ACCOUNT_RESET_NEW_COUNT_ID: 1887 id = uri.getPathSegments().get(1); 1888 if (cache != null) { 1889 cache.lock(id); 1890 } 1891 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1892 if (values != null) { 1893 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1894 if (set != null) { 1895 newMessageCount = new ContentValues(); 1896 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1897 } 1898 } 1899 try { 1900 result = db.update(tableName, newMessageCount, 1901 whereWithId(id, selection), selectionArgs); 1902 } finally { 1903 if (cache != null) { 1904 cache.unlock(id, values); 1905 } 1906 } 1907 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1908 break; 1909 case ACCOUNT_RESET_NEW_COUNT: 1910 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1911 selection, selectionArgs); 1912 // Affects all accounts. Just invalidate all account cache. 1913 cache.invalidate("Reset all new counts", null, null); 1914 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1915 break; 1916 default: 1917 throw new IllegalArgumentException("Unknown URI " + uri); 1918 } 1919 } catch (SQLiteException e) { 1920 checkDatabases(); 1921 throw e; 1922 } 1923 1924 // Notify all notifier cursors 1925 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1926 1927 resolver.notifyChange(notificationUri, null); 1928 return result; 1929 } 1930 1931 /** 1932 * Returns the base notification URI for the given content type. 1933 * 1934 * @param match The type of content that was modified. 1935 */ 1936 private Uri getBaseNotificationUri(int match) { 1937 Uri baseUri = null; 1938 switch (match) { 1939 case MESSAGE: 1940 case MESSAGE_ID: 1941 case SYNCED_MESSAGE_ID: 1942 baseUri = Message.NOTIFIER_URI; 1943 break; 1944 case ACCOUNT: 1945 case ACCOUNT_ID: 1946 baseUri = Account.NOTIFIER_URI; 1947 break; 1948 } 1949 return baseUri; 1950 } 1951 1952 /** 1953 * Sends a change notification to any cursors observers of the given base URI. The final 1954 * notification URI is dynamically built to contain the specified information. It will be 1955 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 1956 * upon the given values. 1957 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 1958 * If this is necessary, it can be added. However, due to the implementation of 1959 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 1960 * 1961 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 1962 * @param op Optional operation to be appended to the URI. 1963 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 1964 * appended to the base URI. 1965 */ 1966 private void sendNotifierChange(Uri baseUri, String op, String id) { 1967 if (baseUri == null) return; 1968 1969 final ContentResolver resolver = getContext().getContentResolver(); 1970 1971 // Append the operation, if specified 1972 if (op != null) { 1973 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 1974 } 1975 1976 long longId = 0L; 1977 try { 1978 longId = Long.valueOf(id); 1979 } catch (NumberFormatException ignore) {} 1980 if (longId > 0) { 1981 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 1982 } else { 1983 resolver.notifyChange(baseUri, null); 1984 } 1985 1986 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 1987 if (baseUri.equals(Message.NOTIFIER_URI)) { 1988 sendMessageListDataChangedNotification(); 1989 } 1990 } 1991 1992 private void sendMessageListDataChangedNotification() { 1993 final Context context = getContext(); 1994 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 1995 // Ideally this intent would contain information about which account changed, to limit the 1996 // updates to that particular account. Unfortunately, that information is not available in 1997 // sendNotifierChange(). 1998 context.sendBroadcast(intent); 1999 } 2000 2001 @Override 2002 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2003 throws OperationApplicationException { 2004 Context context = getContext(); 2005 SQLiteDatabase db = getDatabase(context); 2006 db.beginTransaction(); 2007 try { 2008 ContentProviderResult[] results = super.applyBatch(operations); 2009 db.setTransactionSuccessful(); 2010 return results; 2011 } finally { 2012 db.endTransaction(); 2013 } 2014 } 2015 2016 /** 2017 * For testing purposes, check whether a given row is cached 2018 * @param baseUri the base uri of the EmailContent 2019 * @param id the row id of the EmailContent 2020 * @return whether or not the row is currently cached 2021 */ 2022 @VisibleForTesting 2023 protected boolean isCached(Uri baseUri, long id) { 2024 int match = findMatch(baseUri, "isCached"); 2025 int table = match >> BASE_SHIFT; 2026 ContentCache cache = mContentCaches[table]; 2027 if (cache == null) return false; 2028 Cursor cc = cache.get(Long.toString(id)); 2029 return (cc != null); 2030 } 2031 2032 public static interface AttachmentService { 2033 /** 2034 * Notify the service that an attachment has changed. 2035 */ 2036 void attachmentChanged(Context context, long id, int flags); 2037 } 2038 2039 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 2040 @Override 2041 public void attachmentChanged(Context context, long id, int flags) { 2042 // The default implementation delegates to the real service. 2043 AttachmentDownloadService.attachmentChanged(context, id, flags); 2044 } 2045 }; 2046 private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 2047 2048 /** 2049 * Injects a custom attachment service handler. If null is specified, will reset to the 2050 * default service. 2051 */ 2052 public void injectAttachmentService(AttachmentService as) { 2053 mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; 2054 } 2055 2056 // SELECT DISTINCT Boxes._id, Boxes.unreadCount count(Message._id) from Message, 2057 // (SELECT _id, unreadCount, messageCount, lastNotifiedMessageCount, lastNotifiedMessageKey 2058 // FROM Mailbox WHERE accountKey=6 AND ((type = 0) OR (syncInterval!=0 AND syncInterval!=-1))) 2059 // AS Boxes 2060 // WHERE Boxes.messageCount!=Boxes.lastNotifiedMessageCount 2061 // OR (Boxes._id=Message.mailboxKey AND Message._id>Boxes.lastNotifiedMessageKey) 2062 // TODO: This query can be simplified a bit 2063 private static final String NOTIFICATION_QUERY = 2064 "SELECT DISTINCT Boxes." + MailboxColumns.ID + ", Boxes." + MailboxColumns.UNREAD_COUNT + 2065 ", count(" + Message.TABLE_NAME + "." + MessageColumns.ID + ")" + 2066 " FROM " + 2067 Message.TABLE_NAME + "," + 2068 "(SELECT " + MailboxColumns.ID + "," + MailboxColumns.UNREAD_COUNT + "," + 2069 MailboxColumns.MESSAGE_COUNT + "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT + 2070 "," + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + " FROM " + Mailbox.TABLE_NAME + 2071 " WHERE " + MailboxColumns.ACCOUNT_KEY + "=?" + 2072 " AND (" + MailboxColumns.TYPE + "=" + Mailbox.TYPE_INBOX + " OR (" 2073 + MailboxColumns.SYNC_INTERVAL + "!=0 AND " + 2074 MailboxColumns.SYNC_INTERVAL + "!=-1))) AS Boxes " + 2075 "WHERE Boxes." + MailboxColumns.ID + '=' + Message.TABLE_NAME + "." + 2076 MessageColumns.MAILBOX_KEY + " AND " + Message.TABLE_NAME + "." + 2077 MessageColumns.ID + ">Boxes." + MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY + 2078 " AND " + MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.TIMESTAMP + "!=0"; 2079 2080 public Cursor notificationQuery(Uri uri) { 2081 SQLiteDatabase db = getDatabase(getContext()); 2082 String accountId = uri.getLastPathSegment(); 2083 return db.rawQuery(NOTIFICATION_QUERY, new String[] {accountId}); 2084 } 2085 2086 public Cursor mostRecentMessageQuery(Uri uri) { 2087 SQLiteDatabase db = getDatabase(getContext()); 2088 String mailboxId = uri.getLastPathSegment(); 2089 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 2090 new String[] {mailboxId}); 2091 } 2092 2093 /** 2094 * Support for UnifiedEmail below 2095 */ 2096 2097 private static final String NOT_A_DRAFT_STRING = 2098 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 2099 2100 private static final String CONVERSATION_FLAGS = 2101 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2102 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + 2103 " ELSE 0 END + " + 2104 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + 2105 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + 2106 " ELSE 0 END + " + 2107 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + 2108 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + 2109 " ELSE 0 END"; 2110 2111 /** 2112 * Array of pre-defined account colors (legacy colors from old email app) 2113 */ 2114 private static final int[] ACCOUNT_COLORS = new int[] { 2115 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, 2116 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 2117 }; 2118 2119 private static final String CONVERSATION_COLOR = 2120 "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + 2121 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2122 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2123 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2124 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2125 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2126 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2127 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2128 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2129 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2130 " END"; 2131 2132 private static final String ACCOUNT_COLOR = 2133 "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length + 2134 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2135 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2136 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2137 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2138 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2139 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2140 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2141 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2142 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2143 " END"; 2144 /** 2145 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 2146 * conversation list in UnifiedEmail) 2147 */ 2148 private ProjectionMap getMessageListMap() { 2149 if (sMessageListMap == null) { 2150 sMessageListMap = ProjectionMap.builder() 2151 .add(BaseColumns._ID, MessageColumns.ID) 2152 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 2153 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 2154 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 2155 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 2156 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) 2157 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 2158 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 2159 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 2160 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 2161 .add(UIProvider.ConversationColumns.SENDING_STATE, 2162 Integer.toString(ConversationSendingState.OTHER)) 2163 .add(UIProvider.ConversationColumns.PRIORITY, 2164 Integer.toString(ConversationPriority.LOW)) 2165 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 2166 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 2167 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) 2168 .add(UIProvider.ConversationColumns.ACCOUNT_URI, 2169 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2170 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.DISPLAY_NAME) 2171 .build(); 2172 } 2173 return sMessageListMap; 2174 } 2175 private static ProjectionMap sMessageListMap; 2176 2177 /** 2178 * Generate UIProvider draft type; note the test for "reply all" must come before "reply" 2179 */ 2180 private static final String MESSAGE_DRAFT_TYPE = 2181 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + 2182 ") !=0 THEN " + UIProvider.DraftType.COMPOSE + 2183 " WHEN (" + MessageColumns.FLAGS + "&" + (1<<20) + 2184 ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + 2185 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + 2186 ") !=0 THEN " + UIProvider.DraftType.REPLY + 2187 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + 2188 ") !=0 THEN " + UIProvider.DraftType.FORWARD + 2189 " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; 2190 2191 private static final String MESSAGE_FLAGS = 2192 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2193 ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + 2194 " ELSE 0 END"; 2195 2196 /** 2197 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 2198 * UnifiedEmail 2199 */ 2200 private ProjectionMap getMessageViewMap() { 2201 if (sMessageViewMap == null) { 2202 sMessageViewMap = ProjectionMap.builder() 2203 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 2204 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 2205 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 2206 .add(UIProvider.MessageColumns.CONVERSATION_ID, 2207 uriWithFQId("uimessage", Message.TABLE_NAME)) 2208 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 2209 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 2210 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 2211 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 2212 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 2213 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 2214 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 2215 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, 2216 EmailContent.MessageColumns.TIMESTAMP) 2217 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 2218 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 2219 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 2220 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 2221 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 2222 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, 2223 EmailContent.MessageColumns.FLAG_ATTACHMENT) 2224 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 2225 uriWithFQId("uiattachments", Message.TABLE_NAME)) 2226 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) 2227 .add(UIProvider.MessageColumns.SAVE_MESSAGE_URI, 2228 uriWithFQId("uiupdatedraft", Message.TABLE_NAME)) 2229 .add(UIProvider.MessageColumns.SEND_MESSAGE_URI, 2230 uriWithFQId("uisenddraft", Message.TABLE_NAME)) 2231 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) 2232 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, 2233 uriWithColumn("account", MessageColumns.ACCOUNT_KEY)) 2234 .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) 2235 .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) 2236 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) 2237 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, 2238 Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) 2239 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, 2240 Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) 2241 .add(UIProvider.MessageColumns.VIA_DOMAIN, null) 2242 .build(); 2243 } 2244 return sMessageViewMap; 2245 } 2246 private static ProjectionMap sMessageViewMap; 2247 2248 /** 2249 * Generate UIProvider folder capabilities from mailbox flags 2250 */ 2251 private static final String FOLDER_CAPABILITIES = 2252 "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 2253 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 2254 " ELSE 0 END"; 2255 2256 /** 2257 * Convert EmailProvider type to UIProvider type 2258 */ 2259 private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE 2260 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX 2261 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT 2262 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX 2263 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT 2264 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH 2265 + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM 2266 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED 2267 + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; 2268 2269 private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE 2270 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_folder_inbox_holo_light 2271 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_folder_drafts_holo_light 2272 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_folder_outbox_holo_light 2273 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_folder_sent_holo_light 2274 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_menu_star_holo_light 2275 + " ELSE -1 END"; 2276 2277 private ProjectionMap getFolderListMap() { 2278 if (sFolderListMap == null) { 2279 sFolderListMap = ProjectionMap.builder() 2280 .add(BaseColumns._ID, MailboxColumns.ID) 2281 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 2282 .add(UIProvider.FolderColumns.NAME, "displayName") 2283 .add(UIProvider.FolderColumns.HAS_CHILDREN, 2284 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 2285 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 2286 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 2287 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2288 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2289 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2290 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.MESSAGE_COUNT) 2291 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId("uirefresh")) 2292 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2293 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2294 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 2295 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 2296 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 2297 .build(); 2298 } 2299 return sFolderListMap; 2300 } 2301 private static ProjectionMap sFolderListMap; 2302 2303 /** 2304 * Constructs the map of default entries for accounts. These values can be overridden in 2305 * {@link #genQueryAccount(String[], String)}. 2306 */ 2307 private ProjectionMap getAccountListMap() { 2308 if (sAccountListMap == null) { 2309 sAccountListMap = ProjectionMap.builder() 2310 .add(BaseColumns._ID, AccountColumns.ID) 2311 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2312 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 2313 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2314 .add(UIProvider.AccountColumns.SAVE_DRAFT_URI, uriWithId("uisavedraft")) 2315 .add(UIProvider.AccountColumns.SEND_MAIL_URI, uriWithId("uisendmail")) 2316 .add(UIProvider.AccountColumns.UNDO_URI, 2317 ("'content://" + UIProvider.AUTHORITY + "/uiundo'")) 2318 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2319 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2320 // TODO: Is provider version used? 2321 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2322 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2323 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, uriWithId("uirecentfolders")) 2324 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 2325 uriWithId("uidefaultrecentfolders")) 2326 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2327 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 2328 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2329 .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, 2330 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY)) 2331 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 2332 .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, 2333 Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) 2334 .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null) 2335 .build(); 2336 } 2337 return sAccountListMap; 2338 } 2339 private static ProjectionMap sAccountListMap; 2340 2341 /** 2342 * The "ORDER BY" clause for top level folders 2343 */ 2344 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2345 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2346 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2347 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2348 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2349 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2350 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2351 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2352 + " ELSE 10 END" 2353 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2354 2355 /** 2356 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2357 */ 2358 private ProjectionMap getAttachmentMap() { 2359 if (sAttachmentMap == null) { 2360 sAttachmentMap = ProjectionMap.builder() 2361 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2362 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2363 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2364 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2365 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2366 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2367 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, 2368 AttachmentColumns.UI_DOWNLOADED_SIZE) 2369 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2370 .build(); 2371 } 2372 return sAttachmentMap; 2373 } 2374 private static ProjectionMap sAttachmentMap; 2375 2376 /** 2377 * Generate the SELECT clause using a specified mapping and the original UI projection 2378 * @param map the ProjectionMap to use for this projection 2379 * @param projection the projection as sent by UnifiedEmail 2380 * @param values ContentValues to be used if the ProjectionMap entry is null 2381 * @return a StringBuilder containing the SELECT expression for a SQLite query 2382 */ 2383 private StringBuilder genSelect(ProjectionMap map, String[] projection) { 2384 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2385 } 2386 2387 private StringBuilder genSelect(ProjectionMap map, String[] projection, ContentValues values) { 2388 StringBuilder sb = new StringBuilder("SELECT "); 2389 boolean first = true; 2390 for (String column: projection) { 2391 if (first) { 2392 first = false; 2393 } else { 2394 sb.append(','); 2395 } 2396 String val = null; 2397 // First look at values; this is an override of default behavior 2398 if (values.containsKey(column)) { 2399 String value = values.getAsString(column); 2400 if (value == null) { 2401 val = "NULL AS " + column; 2402 } else if (value.startsWith("@")) { 2403 val = value.substring(1) + " AS " + column; 2404 } else { 2405 val = "'" + value + "' AS " + column; 2406 } 2407 } else { 2408 // Now, get the standard value for the column from our projection map 2409 val = map.get(column); 2410 // If we don't have the column, return "NULL AS <column>", and warn 2411 if (val == null) { 2412 val = "NULL AS " + column; 2413 } 2414 } 2415 sb.append(val); 2416 } 2417 return sb; 2418 } 2419 2420 /** 2421 * Convenience method to create a Uri string given the "type" of query; we append the type 2422 * of the query and the id column name (_id) 2423 * 2424 * @param type the "type" of the query, as defined by our UriMatcher definitions 2425 * @return a Uri string 2426 */ 2427 private static String uriWithId(String type) { 2428 return uriWithColumn(type, EmailContent.RECORD_ID); 2429 } 2430 2431 /** 2432 * Convenience method to create a Uri string given the "type" of query; we append the type 2433 * of the query and the passed in column name 2434 * 2435 * @param type the "type" of the query, as defined by our UriMatcher definitions 2436 * @param columnName the column in the table being queried 2437 * @return a Uri string 2438 */ 2439 private static String uriWithColumn(String type, String columnName) { 2440 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 2441 } 2442 2443 /** 2444 * Convenience method to create a Uri string given the "type" of query and the table name to 2445 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2446 * (i.e. including the table name); we need this for join queries where _id would otherwise 2447 * be ambiguous 2448 * 2449 * @param type the "type" of the query, as defined by our UriMatcher definitions 2450 * @param tableName the name of the table whose _id is referred to 2451 * @return a Uri string 2452 */ 2453 private static String uriWithFQId(String type, String tableName) { 2454 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2455 } 2456 2457 // Regex that matches start of img tag. '<(?i)img\s+'. 2458 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 2459 2460 /** 2461 * Class that holds the sqlite query and the attachment (JSON) value (which might be null) 2462 */ 2463 private static class MessageQuery { 2464 final String query; 2465 final String attachmentJson; 2466 2467 MessageQuery(String _query, String _attachmentJson) { 2468 query = _query; 2469 attachmentJson = _attachmentJson; 2470 } 2471 } 2472 2473 /** 2474 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2475 * 2476 * @param uiProjection as passed from UnifiedEmail 2477 * @return the SQLite query to be executed on the EmailProvider database 2478 */ 2479 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 2480 Context context = getContext(); 2481 long messageId = Long.parseLong(id); 2482 Message msg = Message.restoreMessageWithId(context, messageId); 2483 ContentValues values = new ContentValues(); 2484 String attachmentJson = null; 2485 if (msg != null) { 2486 Body body = Body.restoreBodyWithMessageId(context, messageId); 2487 if (body != null) { 2488 if (body.mHtmlContent != null) { 2489 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2490 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2491 } 2492 } 2493 } 2494 Address[] fromList = Address.unpack(msg.mFrom); 2495 int autoShowImages = 0; 2496 Preferences prefs = Preferences.getPreferences(context); 2497 for (Address sender : fromList) { 2498 String email = sender.getAddress(); 2499 if (prefs.shouldShowImagesFor(email)) { 2500 autoShowImages = 1; 2501 break; 2502 } 2503 } 2504 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2505 // Add attachments... 2506 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2507 if (atts.length > 0) { 2508 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2509 new ArrayList<com.android.mail.providers.Attachment>(); 2510 for (Attachment att : atts) { 2511 if (att.mContentId != null && att.getContentUri() != null) { 2512 continue; 2513 } 2514 com.android.mail.providers.Attachment uiAtt = 2515 new com.android.mail.providers.Attachment(); 2516 uiAtt.name = att.mFileName; 2517 uiAtt.contentType = att.mMimeType; 2518 uiAtt.size = (int) att.mSize; 2519 uiAtt.uri = uiUri("uiattachment", att.mId); 2520 uiAtts.add(uiAtt); 2521 } 2522 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 2523 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 2524 } 2525 if (msg.mDraftInfo != 0) { 2526 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2527 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2528 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2529 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2530 } 2531 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2532 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2533 "content://ui.email2.android.com/event/" + msg.mId); 2534 } 2535 } 2536 StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); 2537 sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + 2538 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + 2539 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2540 String sql = sb.toString(); 2541 return new MessageQuery(sql, attachmentJson); 2542 } 2543 2544 /** 2545 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2546 * 2547 * @param uiProjection as passed from UnifiedEmail 2548 * @return the SQLite query to be executed on the EmailProvider database 2549 */ 2550 private String genQueryMailboxMessages(String[] uiProjection) { 2551 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2552 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=? ORDER BY " + 2553 MessageColumns.TIMESTAMP + " DESC"); 2554 return sb.toString(); 2555 } 2556 2557 /** 2558 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2559 * 2560 * @param uiProjection as passed from UnifiedEmail 2561 * @param id the id of the virtual mailbox 2562 * @return the SQLite query to be executed on the EmailProvider database 2563 */ 2564 private Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2565 long mailboxId) { 2566 ContentValues values = new ContentValues(); 2567 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2568 StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); 2569 if (isCombinedMailbox(mailboxId)) { 2570 switch (getVirtualMailboxType(mailboxId)) { 2571 case Mailbox.TYPE_INBOX: 2572 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2573 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2574 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2575 "=" + Mailbox.TYPE_INBOX + ") ORDER BY " + MessageColumns.TIMESTAMP + 2576 " DESC"); 2577 break; 2578 case Mailbox.TYPE_STARRED: 2579 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2580 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2581 MessageColumns.TIMESTAMP + " DESC"); 2582 break; 2583 default: 2584 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2585 } 2586 return db.rawQuery(sb.toString(), null); 2587 } else { 2588 switch (getVirtualMailboxType(mailboxId)) { 2589 case Mailbox.TYPE_STARRED: 2590 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2591 MessageColumns.ACCOUNT_KEY + "=? AND " + 2592 MessageColumns.FLAG_FAVORITE + "=1 ORDER BY " + 2593 MessageColumns.TIMESTAMP + " DESC"); 2594 break; 2595 default: 2596 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2597 } 2598 return db.rawQuery(sb.toString(), 2599 new String[] {getVirtualMailboxAccountIdString(mailboxId)}); 2600 } 2601 } 2602 2603 /** 2604 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2605 * 2606 * @param uiProjection as passed from UnifiedEmail 2607 * @return the SQLite query to be executed on the EmailProvider database 2608 */ 2609 private String genQueryConversation(String[] uiProjection) { 2610 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2611 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2612 return sb.toString(); 2613 } 2614 2615 /** 2616 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2617 * 2618 * @param uiProjection as passed from UnifiedEmail 2619 * @return the SQLite query to be executed on the EmailProvider database 2620 */ 2621 private String genQueryAccountMailboxes(String[] uiProjection) { 2622 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2623 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2624 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2625 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2626 sb.append(MAILBOX_ORDER_BY); 2627 return sb.toString(); 2628 } 2629 2630 /** 2631 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2632 * sorted by the name as it appears in a hierarchical listing 2633 * 2634 * @param uiProjection as passed from UnifiedEmail 2635 * @return the SQLite query to be executed on the EmailProvider database 2636 */ 2637 private String genQueryAccountAllMailboxes(String[] uiProjection) { 2638 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2639 // Use a derived column to choose either hierarchicalName or displayName 2640 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2641 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2642 " end as h_name"); 2643 // Order by the derived column 2644 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2645 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2646 " ORDER BY h_name"); 2647 return sb.toString(); 2648 } 2649 2650 /** 2651 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2652 * 2653 * @param uiProjection as passed from UnifiedEmail 2654 * @return the SQLite query to be executed on the EmailProvider database 2655 */ 2656 private String genQueryRecentMailboxes(String[] uiProjection) { 2657 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2658 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2659 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2660 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2661 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2662 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2663 return sb.toString(); 2664 } 2665 2666 private int getFolderCapabilities(EmailServiceInfo info, int flags, int type, long mailboxId) { 2667 // All folders support delete 2668 int caps = UIProvider.FolderCapabilities.DELETE; 2669 if (info != null && info.offerLookback) { 2670 // Protocols supporting lookback support settings 2671 caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2672 if ((flags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) { 2673 // If the mailbox can accept moved mail, report that as well 2674 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2675 } 2676 } 2677 // For trash, we don't allow undo 2678 if (type == Mailbox.TYPE_TRASH) { 2679 caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2680 UIProvider.FolderCapabilities.CAN_HOLD_MAIL | 2681 UIProvider.FolderCapabilities.DELETE | 2682 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; 2683 } 2684 if (isVirtualMailbox(mailboxId)) { 2685 caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; 2686 } 2687 return caps; 2688 } 2689 2690 /** 2691 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2692 * 2693 * @param uiProjection as passed from UnifiedEmail 2694 * @return the SQLite query to be executed on the EmailProvider database 2695 */ 2696 private String genQueryMailbox(String[] uiProjection, String id) { 2697 long mailboxId = Long.parseLong(id); 2698 ContentValues values = new ContentValues(); 2699 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2700 // This is the current search mailbox; use the total count 2701 values = new ContentValues(); 2702 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2703 // "load more" is valid for search results 2704 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2705 uiUriString("uiloadmore", mailboxId)); 2706 } else { 2707 Context context = getContext(); 2708 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2709 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2710 if (mailbox != null) { 2711 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2712 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2713 // All folders support delete 2714 if (info != null && info.offerLoadMore) { 2715 // "load more" is valid for protocols not supporting "lookback" 2716 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2717 uiUriString("uiloadmore", mailboxId)); 2718 } 2719 values.put(UIProvider.FolderColumns.CAPABILITIES, 2720 getFolderCapabilities(info, mailbox.mFlags, mailbox.mType, mailboxId)); 2721 } 2722 } 2723 StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); 2724 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2725 return sb.toString(); 2726 } 2727 2728 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://ui.email.android.com"); 2729 2730 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2731 2732 private static String getExternalUriString(String segment, String account) { 2733 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2734 .appendQueryParameter("account", account).build().toString(); 2735 } 2736 2737 private static String getExternalUriStringEmail2(String segment, String account) { 2738 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2739 .appendQueryParameter("account", account).build().toString(); 2740 } 2741 2742 private String getBits(int bitField) { 2743 StringBuilder sb = new StringBuilder(" "); 2744 for (int i = 0; i < 32; i++, bitField >>= 1) { 2745 if ((bitField & 1) != 0) { 2746 sb.append("" + i + " "); 2747 } 2748 } 2749 return sb.toString(); 2750 } 2751 2752 private int getCapabilities(Context context, long accountId) { 2753 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 2754 mServiceCallback, accountId); 2755 int capabilities = 0; 2756 Account acct = null; 2757 try { 2758 service.setTimeout(10); 2759 acct = Account.restoreAccountWithId(context, accountId); 2760 if (acct == null) { 2761 Log.d(TAG, "getCapabilities() for " + accountId + ": returning 0x0 (no account)"); 2762 return 0; 2763 } 2764 capabilities = service.getCapabilities(acct); 2765 // STOPSHIP 2766 Log.d(TAG, "getCapabilities() for " + acct.mDisplayName + ": 0x" + 2767 Integer.toHexString(capabilities) + getBits(capabilities)); 2768 } catch (RemoteException e) { 2769 // Nothing to do 2770 Log.w(TAG, "getCapabilities() for " + acct.mDisplayName + ": RemoteException"); 2771 } 2772 return capabilities; 2773 } 2774 2775 /** 2776 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2777 * 2778 * @param uiProjection as passed from UnifiedEmail 2779 * @return the SQLite query to be executed on the EmailProvider database 2780 */ 2781 private String genQueryAccount(String[] uiProjection, String id) { 2782 final ContentValues values = new ContentValues(); 2783 final long accountId = Long.parseLong(id); 2784 final Context context = getContext(); 2785 2786 final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection); 2787 2788 if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { 2789 // Get account capabilities from the service 2790 values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId)); 2791 } 2792 if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2793 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2794 getExternalUriString("settings", id)); 2795 } 2796 if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { 2797 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2798 getExternalUriStringEmail2("compose", id)); 2799 } 2800 if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { 2801 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2802 } 2803 if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { 2804 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2805 } 2806 2807 final Preferences prefs = Preferences.getPreferences(getContext()); 2808 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 2809 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 2810 prefs.getConfirmDelete() ? "1" : "0"); 2811 } 2812 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 2813 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 2814 prefs.getConfirmSend() ? "1" : "0"); 2815 } 2816 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) { 2817 values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE, 2818 prefs.getSwipeDelete() ? SWIPE_DELETE : SWIPE_DISABLED); 2819 } 2820 if (projectionColumns.contains( 2821 UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) { 2822 values.put(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES, 2823 prefs.getHideCheckboxes() ? "1" : "0"); 2824 } 2825 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 2826 int autoAdvance = prefs.getAutoAdvanceDirection(); 2827 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 2828 autoAdvanceToUiValue(autoAdvance)); 2829 } 2830 if (projectionColumns.contains( 2831 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 2832 int textZoom = prefs.getTextZoom(); 2833 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 2834 textZoomToUiValue(textZoom)); 2835 } 2836 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 2837 long mailboxId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 2838 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && 2839 mailboxId != Mailbox.NO_MAILBOX) { 2840 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 2841 uiUriString("uifolder", mailboxId)); 2842 } 2843 if (projectionColumns.contains( 2844 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) && 2845 mailboxId != Mailbox.NO_MAILBOX) { 2846 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, 2847 Mailbox.getDisplayName(context, mailboxId)); 2848 } 2849 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) { 2850 if (mailboxId != Mailbox.NO_MAILBOX) { 2851 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 2852 } else { 2853 values.put(UIProvider.AccountColumns.SYNC_STATUS, 2854 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 2855 } 2856 } 2857 if (projectionColumns.contains( 2858 UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) { 2859 // Email doesn't support priority inbox, so always state priority arrows disabled. 2860 values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0"); 2861 } 2862 if (projectionColumns.contains( 2863 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) { 2864 // Set the setup intent if needed 2865 // TODO We should clarify/document the trash/setup relationship 2866 long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH); 2867 if (trashId == Mailbox.NO_MAILBOX) { 2868 EmailServiceInfo info = EmailServiceUtils.getServiceInfoForAccount(context, 2869 accountId); 2870 if (info != null && info.requiresSetup) { 2871 values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI, 2872 getExternalUriString("setup", id)); 2873 } 2874 } 2875 } 2876 final StringBuilder sb = genSelect(getAccountListMap(), uiProjection, values); 2877 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2878 return sb.toString(); 2879 } 2880 2881 private int autoAdvanceToUiValue(int autoAdvance) { 2882 switch(autoAdvance) { 2883 case Preferences.AUTO_ADVANCE_OLDER: 2884 return UIProvider.AutoAdvance.OLDER; 2885 case Preferences.AUTO_ADVANCE_NEWER: 2886 return UIProvider.AutoAdvance.NEWER; 2887 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 2888 default: 2889 return UIProvider.AutoAdvance.LIST; 2890 } 2891 } 2892 2893 private int textZoomToUiValue(int textZoom) { 2894 switch(textZoom) { 2895 case Preferences.TEXT_ZOOM_HUGE: 2896 return UIProvider.MessageTextSize.HUGE; 2897 case Preferences.TEXT_ZOOM_LARGE: 2898 return UIProvider.MessageTextSize.LARGE; 2899 case Preferences.TEXT_ZOOM_NORMAL: 2900 return UIProvider.MessageTextSize.NORMAL; 2901 case Preferences.TEXT_ZOOM_SMALL: 2902 return UIProvider.MessageTextSize.SMALL; 2903 case Preferences.TEXT_ZOOM_TINY: 2904 return UIProvider.MessageTextSize.TINY; 2905 default: 2906 return UIProvider.MessageTextSize.NORMAL; 2907 } 2908 } 2909 2910 /** 2911 * Generate a Uri string for a combined mailbox uri 2912 * @param type the uri command type (e.g. "uimessages") 2913 * @param id the id of the item (e.g. an account, mailbox, or message id) 2914 * @return a Uri string 2915 */ 2916 private static String combinedUriString(String type, String id) { 2917 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 2918 } 2919 2920 private static final long COMBINED_ACCOUNT_ID = 0x10000000; 2921 2922 /** 2923 * Generate an id for a combined mailbox of a given type 2924 * @param type the mailbox type for the combined mailbox 2925 * @return the id, as a String 2926 */ 2927 private static String combinedMailboxId(int type) { 2928 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 2929 } 2930 2931 private static String getVirtualMailboxIdString(long accountId, int type) { 2932 return Long.toString(getVirtualMailboxId(accountId, type)); 2933 } 2934 2935 private static long getVirtualMailboxId(long accountId, int type) { 2936 return (accountId << 32) + type; 2937 } 2938 2939 private static boolean isVirtualMailbox(long mailboxId) { 2940 return mailboxId >= 0x100000000L; 2941 } 2942 2943 private static boolean isCombinedMailbox(long mailboxId) { 2944 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 2945 } 2946 2947 private static long getVirtualMailboxAccountId(long mailboxId) { 2948 return mailboxId >> 32; 2949 } 2950 2951 private static String getVirtualMailboxAccountIdString(long mailboxId) { 2952 return Long.toString(mailboxId >> 32); 2953 } 2954 2955 private static int getVirtualMailboxType(long mailboxId) { 2956 return (int)(mailboxId & 0xF); 2957 } 2958 2959 private void addCombinedAccountRow(MatrixCursor mc) { 2960 final long id = Account.getDefaultAccountId(getContext()); 2961 if (id == Account.NO_ACCOUNT) return; 2962 final String idString = Long.toString(id); 2963 2964 // Build a map of the requested columns to the appropriate positions 2965 final ImmutableMap.Builder<String, Integer> builder = 2966 new ImmutableMap.Builder<String, Integer>(); 2967 final String[] columnNames = mc.getColumnNames(); 2968 for (int i = 0; i < columnNames.length; i++) { 2969 builder.put(columnNames[i], i); 2970 } 2971 final Map<String, Integer> colPosMap = builder.build(); 2972 2973 final Object[] values = new Object[columnNames.length]; 2974 if (colPosMap.containsKey(BaseColumns._ID)) { 2975 values[colPosMap.get(BaseColumns._ID)] = 0; 2976 } 2977 if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) { 2978 values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] = 2979 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 2980 } 2981 if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) { 2982 values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] = 2983 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 2984 } 2985 if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) { 2986 values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString( 2987 R.string.mailbox_list_account_selector_combined_view); 2988 } 2989 if (colPosMap.containsKey(UIProvider.AccountColumns.SAVE_DRAFT_URI)) { 2990 values[colPosMap.get(UIProvider.AccountColumns.SAVE_DRAFT_URI)] = 2991 combinedUriString("uisavedraft", idString); 2992 } 2993 if (colPosMap.containsKey(UIProvider.AccountColumns.SEND_MAIL_URI)) { 2994 values[colPosMap.get(UIProvider.AccountColumns.SEND_MAIL_URI)] = 2995 combinedUriString("uisendmail", idString); 2996 } 2997 if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) { 2998 values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] = 2999 "'content://" + UIProvider.AUTHORITY + "/uiundo'"; 3000 } 3001 if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) { 3002 values[colPosMap.get(UIProvider.AccountColumns.URI)] = 3003 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 3004 } 3005 if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) { 3006 values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] = 3007 EMAIL_APP_MIME_TYPE; 3008 } 3009 if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 3010 values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] = 3011 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 3012 } 3013 if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) { 3014 values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] = 3015 getExternalUriStringEmail2("compose", Long.toString(id)); 3016 } 3017 3018 // TODO: Get these from default account? 3019 Preferences prefs = Preferences.getPreferences(getContext()); 3020 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 3021 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] = 3022 Integer.toString(UIProvider.AutoAdvance.NEWER); 3023 } 3024 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 3025 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] = 3026 Integer.toString(UIProvider.MessageTextSize.NORMAL); 3027 } 3028 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { 3029 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] = 3030 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 3031 } 3032 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 3033 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 3034 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] = 3035 Integer.toString(UIProvider.DefaultReplyBehavior.REPLY); 3036 } 3037 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) { 3038 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)] = 0; 3039 } 3040 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 3041 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] = 3042 prefs.getConfirmDelete() ? 1 : 0; 3043 } 3044 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { 3045 values[colPosMap.get( 3046 UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0; 3047 } 3048 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 3049 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] = 3050 prefs.getConfirmSend() ? 1 : 0; 3051 } 3052 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)) { 3053 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.HIDE_CHECKBOXES)] = 3054 prefs.getHideCheckboxes() ? 1 : 0; 3055 } 3056 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { 3057 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] = 3058 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 3059 } 3060 3061 mc.addRow(values); 3062 } 3063 3064 private Cursor getVirtualMailboxCursor(long mailboxId) { 3065 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1); 3066 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 3067 getVirtualMailboxType(mailboxId))); 3068 return mc; 3069 } 3070 3071 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 3072 String idString = getVirtualMailboxIdString(accountId, mailboxType); 3073 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 3074 values[UIProvider.FOLDER_ID_COLUMN] = 0; 3075 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 3076 values[UIProvider.FOLDER_NAME_COLUMN] = getMailboxNameForType(mailboxType); 3077 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 3078 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = UIProvider.FolderCapabilities.IS_VIRTUAL; 3079 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 3080 idString); 3081 values[UIProvider.FOLDER_ID_COLUMN] = 0; 3082 return values; 3083 } 3084 3085 private Cursor uiAccounts(String[] uiProjection) { 3086 Context context = getContext(); 3087 SQLiteDatabase db = getDatabase(context); 3088 Cursor accountIdCursor = 3089 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 3090 int numAccounts = accountIdCursor.getCount(); 3091 boolean combinedAccount = false; 3092 if (numAccounts > 1) { 3093 combinedAccount = true; 3094 numAccounts++; 3095 } 3096 final Bundle extras = new Bundle(); 3097 // Email always returns the accurate number of accounts 3098 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 3099 final MatrixCursor mc = 3100 new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 3101 Object[] values = new Object[uiProjection.length]; 3102 try { 3103 while (accountIdCursor.moveToNext()) { 3104 String id = accountIdCursor.getString(0); 3105 Cursor accountCursor = 3106 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3107 if (accountCursor.moveToNext()) { 3108 for (int i = 0; i < uiProjection.length; i++) { 3109 values[i] = accountCursor.getString(i); 3110 } 3111 mc.addRow(values); 3112 } 3113 accountCursor.close(); 3114 } 3115 if (combinedAccount) { 3116 addCombinedAccountRow(mc); 3117 } 3118 } finally { 3119 accountIdCursor.close(); 3120 } 3121 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER); 3122 return mc; 3123 } 3124 3125 /** 3126 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 3127 * 3128 * @param uiProjection as passed from UnifiedEmail 3129 * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments 3130 * or null if there are no query parameters 3131 * @return the SQLite query to be executed on the EmailProvider database 3132 */ 3133 private String genQueryAttachments(String[] uiProjection, 3134 List<String> contentTypeQueryParameters) { 3135 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection); 3136 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + 3137 " =? "); 3138 3139 // Filter for certain content types. 3140 // The filter works by adding LIKE operators for each 3141 // content type you wish to request. Content types 3142 // are filtered by performing a case-insensitive "starts with" 3143 // filter. IE, "image/" would return "image/png" as well as "image/jpeg". 3144 if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) { 3145 final int size = contentTypeQueryParameters.size(); 3146 sb.append("AND ("); 3147 for (int i = 0; i < size; i++) { 3148 final String contentType = contentTypeQueryParameters.get(i); 3149 sb.append(AttachmentColumns.MIME_TYPE + " LIKE '" + contentType + "%'"); 3150 3151 if (i != size - 1) { 3152 sb.append(" OR "); 3153 } 3154 } 3155 sb.append(")"); 3156 } 3157 return sb.toString(); 3158 } 3159 3160 /** 3161 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 3162 * 3163 * @param uiProjection as passed from UnifiedEmail 3164 * @return the SQLite query to be executed on the EmailProvider database 3165 */ 3166 private String genQueryAttachment(String[] uiProjection, String idString) { 3167 Long id = Long.parseLong(idString); 3168 Attachment att = Attachment.restoreAttachmentWithId(getContext(), id); 3169 ContentValues values = new ContentValues(); 3170 values.put(AttachmentColumns.CONTENT_URI, 3171 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString()); 3172 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 3173 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); 3174 return sb.toString(); 3175 } 3176 3177 /** 3178 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 3179 * 3180 * @param uiProjection as passed from UnifiedEmail 3181 * @return the SQLite query to be executed on the EmailProvider database 3182 */ 3183 private String genQuerySubfolders(String[] uiProjection) { 3184 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3185 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 3186 " =? ORDER BY "); 3187 sb.append(MAILBOX_ORDER_BY); 3188 return sb.toString(); 3189 } 3190 3191 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 3192 3193 /** 3194 * Returns a cursor over all the folders for a specific URI which corresponds to a single 3195 * account. 3196 * @param uri 3197 * @param uiProjection 3198 * @return 3199 */ 3200 private Cursor uiFolders(Uri uri, String[] uiProjection) { 3201 Context context = getContext(); 3202 SQLiteDatabase db = getDatabase(context); 3203 String id = uri.getPathSegments().get(1); 3204 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3205 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 2); 3206 Object[] row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX); 3207 int numUnread = EmailContent.count(context, Message.CONTENT_URI, 3208 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 3209 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 3210 "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", null); 3211 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numUnread; 3212 mc.addRow(row); 3213 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 3214 MessageColumns.FLAG_FAVORITE + "=1", null); 3215 if (numStarred > 0) { 3216 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED); 3217 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 3218 mc.addRow(row); 3219 } 3220 return mc; 3221 } else { 3222 Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 3223 c = getFolderListCursor(db, c, uiProjection); 3224 int numStarred = EmailContent.count(context, Message.CONTENT_URI, 3225 MessageColumns.ACCOUNT_KEY + "=? AND " + MessageColumns.FLAG_FAVORITE + "=1", 3226 new String[] {id}); 3227 if (numStarred == 0) { 3228 return c; 3229 } else { 3230 // Add starred virtual folder to the cursor 3231 // Show number of messages as unread count (for backward compatibility) 3232 MatrixCursor starCursor = new MatrixCursorWithCachedColumns(uiProjection, 1); 3233 Object[] row = getVirtualMailboxRow(Long.parseLong(id), Mailbox.TYPE_STARRED); 3234 row[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = numStarred; 3235 row[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_menu_star_holo_light; 3236 starCursor.addRow(row); 3237 Cursor[] cursors = new Cursor[] {starCursor, c}; 3238 return new MergeCursor(cursors); 3239 } 3240 } 3241 } 3242 3243 /** 3244 * Returns an array of the default recent folders for a given URI which is unique for an 3245 * account. Some accounts might not have default recent folders, in which case an empty array 3246 * is returned. 3247 * @param id 3248 * @return 3249 */ 3250 private Uri[] defaultRecentFolders(final String id) { 3251 final SQLiteDatabase db = getDatabase(getContext()); 3252 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3253 // We don't have default recents for the combined view. 3254 return new Uri[0]; 3255 } 3256 // We search for the types we want, and find corresponding IDs. 3257 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 3258 3259 // Sent, Drafts, and Starred are the default recents. 3260 final StringBuilder sb = genSelect(getFolderListMap(), idAndType); 3261 sb.append(" FROM " + Mailbox.TABLE_NAME 3262 + " WHERE " + MailboxColumns.ACCOUNT_KEY + " = " + id 3263 + " AND " 3264 + MailboxColumns.TYPE + " IN (" + Mailbox.TYPE_SENT + 3265 ", " + Mailbox.TYPE_DRAFTS + 3266 ", " + Mailbox.TYPE_STARRED 3267 + ")"); 3268 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 3269 final Cursor c = db.rawQuery(sb.toString(), null); 3270 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 3271 return new Uri[0]; 3272 } 3273 // Read all the IDs of the mailboxes, and turn them into URIs. 3274 final Uri[] recentFolders = new Uri[c.getCount()]; 3275 int i = 0; 3276 do { 3277 final long folderId = c.getLong(0); 3278 recentFolders[i] = uiUri("uifolder", folderId); 3279 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 3280 ++i; 3281 } while (c.moveToNext()); 3282 return recentFolders; 3283 } 3284 3285 /** 3286 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 3287 * any pending notifications for the corresponding mailbox should be canceled). We also handle 3288 * getExtras() to provide a snapshot of the mailbox's status 3289 */ 3290 static class VisibilityCursor extends CursorWrapper { 3291 private final long mMailboxId; 3292 private final Context mContext; 3293 private final Bundle mExtras = new Bundle(); 3294 3295 public VisibilityCursor(Context context, Cursor cursor, long mailboxId) { 3296 super(cursor); 3297 mMailboxId = mailboxId; 3298 mContext = context; 3299 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3300 if (mailbox != null) { 3301 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, mailbox.mUiSyncStatus); 3302 if (mailbox.mUiLastSyncResult != UIProvider.LastSyncResult.SUCCESS) { 3303 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3304 mailbox.mUiLastSyncResult); 3305 } 3306 } 3307 } 3308 3309 @Override 3310 public Bundle getExtras() { 3311 return mExtras; 3312 } 3313 3314 @Override 3315 public Bundle respond(Bundle params) { 3316 final String setVisibilityKey = 3317 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3318 if (params.containsKey(setVisibilityKey)) { 3319 final boolean visible = params.getBoolean(setVisibilityKey); 3320 if (visible) { 3321 NotificationController.getInstance(mContext).cancelNewMessageNotification( 3322 mMailboxId); 3323 // Clear the visible limit of the mailbox (if any) on entry 3324 if (params.containsKey( 3325 UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { 3326 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 3327 if (mailbox != null && mailbox.mVisibleLimit > 0) { 3328 ContentValues values = new ContentValues(); 3329 values.put(MailboxColumns.VISIBLE_LIMIT, 0); 3330 mContext.getContentResolver().update( 3331 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), 3332 values, null, null); 3333 } 3334 } 3335 } 3336 } 3337 // Return success 3338 Bundle response = new Bundle(); 3339 response.putString(setVisibilityKey, 3340 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3341 return response; 3342 } 3343 } 3344 3345 static class AttachmentsCursor extends CursorWrapper { 3346 private final int mContentUriIndex; 3347 private final int mUriIndex; 3348 private final Context mContext; 3349 3350 public AttachmentsCursor(Context context, Cursor cursor) { 3351 super(cursor); 3352 mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI); 3353 mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI); 3354 mContext = context; 3355 } 3356 3357 @Override 3358 public String getString(int column) { 3359 if (column == mContentUriIndex) { 3360 Uri uri = Uri.parse(getString(mUriIndex)); 3361 long id = Long.parseLong(uri.getLastPathSegment()); 3362 Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 3363 if (att == null) return ""; 3364 return AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString(); 3365 } else { 3366 return super.getString(column); 3367 } 3368 } 3369 } 3370 3371 /** 3372 * For debugging purposes; shouldn't be used in production code 3373 */ 3374 static class CloseDetectingCursor extends CursorWrapper { 3375 3376 public CloseDetectingCursor(Cursor cursor) { 3377 super(cursor); 3378 } 3379 3380 @Override 3381 public void close() { 3382 super.close(); 3383 Log.d(TAG, "Closing cursor", new Error()); 3384 } 3385 } 3386 3387 /** 3388 * We need to do individual queries for the mailboxes in order to get correct 3389 * folder capabilities. 3390 */ 3391 Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) { 3392 final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); 3393 Object[] values = new Object[uiProjection.length]; 3394 String[] args = new String[1]; 3395 try { 3396 // Loop through mailboxes, building matrix cursor 3397 while (c.moveToNext()) { 3398 String id = c.getString(0); 3399 args[0] = id; 3400 Cursor mailboxCursor = db.rawQuery(genQueryMailbox(uiProjection, id), args); 3401 if (mailboxCursor.moveToNext()) { 3402 for (int i = 0; i < uiProjection.length; i++) { 3403 values[i] = mailboxCursor.getString(i); 3404 } 3405 mc.addRow(values); 3406 } 3407 } 3408 } finally { 3409 c.close(); 3410 } 3411 return mc; 3412 } 3413 3414 /** 3415 * Handle UnifiedEmail queries here (dispatched from query()) 3416 * 3417 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 3418 * @param uri the original uri passed in from UnifiedEmail 3419 * @param uiProjection the projection passed in from UnifiedEmail 3420 * @return the result Cursor 3421 */ 3422 private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { 3423 Context context = getContext(); 3424 ContentResolver resolver = context.getContentResolver(); 3425 SQLiteDatabase db = getDatabase(context); 3426 // Should we ever return null, or throw an exception?? 3427 Cursor c = null; 3428 String id = uri.getPathSegments().get(1); 3429 Uri notifyUri = null; 3430 switch(match) { 3431 case UI_ALL_FOLDERS: 3432 c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id}); 3433 c = getFolderListCursor(db, c, uiProjection); 3434 break; 3435 case UI_RECENT_FOLDERS: 3436 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 3437 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3438 break; 3439 case UI_SUBFOLDERS: 3440 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 3441 c = getFolderListCursor(db, c, uiProjection); 3442 break; 3443 case UI_MESSAGES: 3444 long mailboxId = Long.parseLong(id); 3445 if (isVirtualMailbox(mailboxId)) { 3446 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId); 3447 } else { 3448 c = db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); 3449 } 3450 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 3451 c = new VisibilityCursor(context, c, mailboxId); 3452 break; 3453 case UI_MESSAGE: 3454 MessageQuery qq = genQueryViewMessage(uiProjection, id); 3455 String sql = qq.query; 3456 String attJson = qq.attachmentJson; 3457 // With attachments, we have another argument to bind 3458 if (attJson != null) { 3459 c = db.rawQuery(sql, new String[] {attJson, id}); 3460 } else { 3461 c = db.rawQuery(sql, new String[] {id}); 3462 } 3463 break; 3464 case UI_ATTACHMENTS: 3465 final List<String> contentTypeQueryParameters = 3466 uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE); 3467 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters), 3468 new String[] {id}); 3469 c = new AttachmentsCursor(context, c); 3470 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 3471 break; 3472 case UI_ATTACHMENT: 3473 c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id}); 3474 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 3475 break; 3476 case UI_FOLDER: 3477 mailboxId = Long.parseLong(id); 3478 if (isVirtualMailbox(mailboxId)) { 3479 c = getVirtualMailboxCursor(mailboxId); 3480 } else { 3481 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[] {id}); 3482 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3483 } 3484 break; 3485 case UI_ACCOUNT: 3486 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3487 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1); 3488 addCombinedAccountRow(mc); 3489 c = mc; 3490 } else { 3491 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3492 } 3493 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 3494 break; 3495 case UI_CONVERSATION: 3496 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 3497 break; 3498 } 3499 if (notifyUri != null) { 3500 c.setNotificationUri(resolver, notifyUri); 3501 } 3502 return c; 3503 } 3504 3505 /** 3506 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 3507 * a few of the fields 3508 * @param uiAtt the UIProvider attachment to convert 3509 * @return the EmailProvider attachment 3510 */ 3511 private Attachment convertUiAttachmentToAttachment( 3512 com.android.mail.providers.Attachment uiAtt) { 3513 Attachment att = new Attachment(); 3514 att.setContentUri(uiAtt.contentUri.toString()); 3515 att.mFileName = uiAtt.name; 3516 att.mMimeType = uiAtt.contentType; 3517 att.mSize = uiAtt.size; 3518 return att; 3519 } 3520 3521 private String getMailboxNameForType(int mailboxType) { 3522 Context context = getContext(); 3523 int resId; 3524 switch (mailboxType) { 3525 case Mailbox.TYPE_INBOX: 3526 resId = R.string.mailbox_name_server_inbox; 3527 break; 3528 case Mailbox.TYPE_OUTBOX: 3529 resId = R.string.mailbox_name_server_outbox; 3530 break; 3531 case Mailbox.TYPE_DRAFTS: 3532 resId = R.string.mailbox_name_server_drafts; 3533 break; 3534 case Mailbox.TYPE_TRASH: 3535 resId = R.string.mailbox_name_server_trash; 3536 break; 3537 case Mailbox.TYPE_SENT: 3538 resId = R.string.mailbox_name_server_sent; 3539 break; 3540 case Mailbox.TYPE_JUNK: 3541 resId = R.string.mailbox_name_server_junk; 3542 break; 3543 case Mailbox.TYPE_STARRED: 3544 resId = R.string.widget_starred; 3545 break; 3546 default: 3547 throw new IllegalArgumentException("Illegal mailbox type"); 3548 } 3549 return context.getString(resId); 3550 } 3551 3552 /** 3553 * Create a mailbox given the account and mailboxType. 3554 */ 3555 private Mailbox createMailbox(long accountId, int mailboxType) { 3556 Context context = getContext(); 3557 Mailbox box = Mailbox.newSystemMailbox(accountId, mailboxType, 3558 getMailboxNameForType(mailboxType)); 3559 // Make sure drafts and save will show up in recents... 3560 // If these already exist (from old Email app), they will have touch times 3561 switch (mailboxType) { 3562 case Mailbox.TYPE_DRAFTS: 3563 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 3564 break; 3565 case Mailbox.TYPE_SENT: 3566 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 3567 break; 3568 } 3569 box.save(context); 3570 return box; 3571 } 3572 3573 /** 3574 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 3575 * @param accountName the account name to use 3576 * @param mailboxType the type of mailbox we're trying to find 3577 * @return the mailbox of the given type for the account in the uri, or null if not found 3578 */ 3579 private Mailbox getMailboxByAccountIdAndType(String accountId, int mailboxType) { 3580 long id = Long.parseLong(accountId); 3581 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), id, mailboxType); 3582 if (mailbox == null) { 3583 mailbox = createMailbox(id, mailboxType); 3584 } 3585 return mailbox; 3586 } 3587 3588 private Message getMessageFromPathSegments(List<String> pathSegments) { 3589 Message msg = null; 3590 if (pathSegments.size() > 2) { 3591 msg = Message.restoreMessageWithId(getContext(), Long.parseLong(pathSegments.get(2))); 3592 } 3593 if (msg == null) { 3594 msg = new Message(); 3595 } 3596 return msg; 3597 } 3598 /** 3599 * Given a mailbox and the content values for a message, create/save the message in the mailbox 3600 * @param mailbox the mailbox to use 3601 * @param values the content values that represent message fields 3602 * @return the uri of the newly created message 3603 */ 3604 private Uri uiSaveMessage(Message msg, Mailbox mailbox, ContentValues values) { 3605 Context context = getContext(); 3606 // Fill in the message 3607 Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 3608 if (account == null) return null; 3609 msg.mFrom = account.mEmailAddress; 3610 msg.mTimeStamp = System.currentTimeMillis(); 3611 msg.mTo = values.getAsString(UIProvider.MessageColumns.TO); 3612 msg.mCc = values.getAsString(UIProvider.MessageColumns.CC); 3613 msg.mBcc = values.getAsString(UIProvider.MessageColumns.BCC); 3614 msg.mSubject = values.getAsString(UIProvider.MessageColumns.SUBJECT); 3615 msg.mText = values.getAsString(UIProvider.MessageColumns.BODY_TEXT); 3616 msg.mHtml = values.getAsString(UIProvider.MessageColumns.BODY_HTML); 3617 msg.mMailboxKey = mailbox.mId; 3618 msg.mAccountKey = mailbox.mAccountKey; 3619 msg.mDisplayName = msg.mTo; 3620 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 3621 msg.mFlagRead = true; 3622 Integer quoteStartPos = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3623 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 3624 int flags = 0; 3625 int draftType = values.getAsInteger(UIProvider.MessageColumns.DRAFT_TYPE); 3626 switch(draftType) { 3627 case DraftType.FORWARD: 3628 flags |= Message.FLAG_TYPE_FORWARD; 3629 break; 3630 case DraftType.REPLY_ALL: 3631 flags |= Message.FLAG_TYPE_REPLY_ALL; 3632 // Fall through 3633 case DraftType.REPLY: 3634 flags |= Message.FLAG_TYPE_REPLY; 3635 break; 3636 case DraftType.COMPOSE: 3637 flags |= Message.FLAG_TYPE_ORIGINAL; 3638 break; 3639 } 3640 int draftInfo = 0; 3641 if (values.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 3642 draftInfo = values.getAsInteger(UIProvider.MessageColumns.QUOTE_START_POS); 3643 if (values.getAsInteger(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 3644 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 3645 } 3646 } 3647 if (!values.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) { 3648 flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT; 3649 } 3650 msg.mDraftInfo = draftInfo; 3651 msg.mFlags = flags; 3652 3653 String ref = values.getAsString(UIProvider.MessageColumns.REF_MESSAGE_ID); 3654 if (ref != null && msg.mQuotedTextStartPos >= 0) { 3655 String refId = Uri.parse(ref).getLastPathSegment(); 3656 try { 3657 long sourceKey = Long.parseLong(refId); 3658 msg.mSourceKey = sourceKey; 3659 } catch (NumberFormatException e) { 3660 // This will be zero; the default 3661 } 3662 } 3663 3664 // Get attachments from the ContentValues 3665 List<com.android.mail.providers.Attachment> uiAtts = 3666 com.android.mail.providers.Attachment.fromJSONArray( 3667 values.getAsString(UIProvider.MessageColumns.JOINED_ATTACHMENT_INFOS)); 3668 ArrayList<Attachment> atts = new ArrayList<Attachment>(); 3669 boolean hasUnloadedAttachments = false; 3670 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 3671 Uri attUri = uiAtt.uri; 3672 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 3673 // If it's one of ours, retrieve the attachment and add it to the list 3674 long attId = Long.parseLong(attUri.getLastPathSegment()); 3675 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 3676 if (att != null) { 3677 // We must clone the attachment into a new one for this message; easiest to 3678 // use a parcel here 3679 Parcel p = Parcel.obtain(); 3680 att.writeToParcel(p, 0); 3681 p.setDataPosition(0); 3682 Attachment attClone = new Attachment(p); 3683 p.recycle(); 3684 // Clear the messageKey (this is going to be a new attachment) 3685 attClone.mMessageKey = 0; 3686 // If we're sending this, it's not loaded, and we're not smart forwarding 3687 // add the download flag, so that ADS will start up 3688 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null && 3689 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 3690 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 3691 hasUnloadedAttachments = true; 3692 } 3693 atts.add(attClone); 3694 } 3695 } else { 3696 // Convert external attachment to one of ours and add to the list 3697 atts.add(convertUiAttachmentToAttachment(uiAtt)); 3698 } 3699 } 3700 if (!atts.isEmpty()) { 3701 msg.mAttachments = atts; 3702 msg.mFlagAttachment = true; 3703 if (hasUnloadedAttachments) { 3704 Utility.showToast(context, R.string.message_view_attachment_background_load); 3705 } 3706 } 3707 // Save it or update it... 3708 if (!msg.isSaved()) { 3709 msg.save(context); 3710 } else { 3711 // This is tricky due to how messages/attachments are saved; rather than putz with 3712 // what's changed, we'll delete/re-add them 3713 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 3714 // Delete all existing attachments 3715 ops.add(ContentProviderOperation.newDelete( 3716 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 3717 .build()); 3718 // Delete the body 3719 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 3720 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 3721 .build()); 3722 // Add the ops for the message, atts, and body 3723 msg.addSaveOps(ops); 3724 // Do it! 3725 try { 3726 applyBatch(ops); 3727 } catch (OperationApplicationException e) { 3728 } 3729 } 3730 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 3731 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 3732 mServiceCallback, mailbox.mAccountKey); 3733 try { 3734 service.startSync(mailbox.mId, true); 3735 } catch (RemoteException e) { 3736 } 3737 long originalMsgId = msg.mSourceKey; 3738 if (originalMsgId != 0) { 3739 Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 3740 // If the original message exists, set its forwarded/replied to flags 3741 if (originalMsg != null) { 3742 ContentValues cv = new ContentValues(); 3743 flags = originalMsg.mFlags; 3744 switch(draftType) { 3745 case DraftType.FORWARD: 3746 flags |= Message.FLAG_FORWARDED; 3747 break; 3748 case DraftType.REPLY_ALL: 3749 case DraftType.REPLY: 3750 flags |= Message.FLAG_REPLIED_TO; 3751 break; 3752 } 3753 cv.put(Message.FLAGS, flags); 3754 context.getContentResolver().update(ContentUris.withAppendedId( 3755 Message.CONTENT_URI, originalMsgId), cv, null, null); 3756 } 3757 } 3758 } 3759 return uiUri("uimessage", msg.mId); 3760 } 3761 3762 /** 3763 * Create and send the message via the account indicated in the uri 3764 * @param uri the incoming uri 3765 * @param values the content values that represent message fields 3766 * @return the uri of the created message 3767 */ 3768 private Uri uiSendMail(Uri uri, ContentValues values) { 3769 List<String> pathSegments = uri.getPathSegments(); 3770 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_OUTBOX); 3771 if (mailbox == null) return null; 3772 Message msg = getMessageFromPathSegments(pathSegments); 3773 try { 3774 return uiSaveMessage(msg, mailbox, values); 3775 } finally { 3776 // Kick observers 3777 getContext().getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3778 } 3779 } 3780 3781 /** 3782 * Create a message and save it to the drafts folder of the account indicated in the uri 3783 * @param uri the incoming uri 3784 * @param values the content values that represent message fields 3785 * @return the uri of the created message 3786 */ 3787 private Uri uiSaveDraft(Uri uri, ContentValues values) { 3788 List<String> pathSegments = uri.getPathSegments(); 3789 Mailbox mailbox = getMailboxByAccountIdAndType(pathSegments.get(1), Mailbox.TYPE_DRAFTS); 3790 if (mailbox == null) return null; 3791 Message msg = getMessageFromPathSegments(pathSegments); 3792 return uiSaveMessage(msg, mailbox, values); 3793 } 3794 3795 private int uiUpdateDraft(Uri uri, ContentValues values) { 3796 Context context = getContext(); 3797 Message msg = Message.restoreMessageWithId(context, 3798 Long.parseLong(uri.getPathSegments().get(1))); 3799 if (msg == null) return 0; 3800 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 3801 if (mailbox == null) return 0; 3802 uiSaveMessage(msg, mailbox, values); 3803 return 1; 3804 } 3805 3806 private int uiSendDraft(Uri uri, ContentValues values) { 3807 Context context = getContext(); 3808 Message msg = Message.restoreMessageWithId(context, 3809 Long.parseLong(uri.getPathSegments().get(1))); 3810 if (msg == null) return 0; 3811 long mailboxId = Mailbox.findMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_OUTBOX); 3812 if (mailboxId == Mailbox.NO_MAILBOX) return 0; 3813 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3814 if (mailbox == null) return 0; 3815 uiSaveMessage(msg, mailbox, values); 3816 // Kick observers 3817 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3818 return 1; 3819 } 3820 3821 private void putIntegerLongOrBoolean(ContentValues values, String columnName, Object value) { 3822 if (value instanceof Integer) { 3823 Integer intValue = (Integer)value; 3824 values.put(columnName, intValue); 3825 } else if (value instanceof Boolean) { 3826 Boolean boolValue = (Boolean)value; 3827 values.put(columnName, boolValue ? 1 : 0); 3828 } else if (value instanceof Long) { 3829 Long longValue = (Long)value; 3830 values.put(columnName, longValue); 3831 } 3832 } 3833 3834 /** 3835 * Update the timestamps for the folders specified and notifies on the recent folder URI. 3836 * @param folders 3837 * @return number of folders updated 3838 */ 3839 private int updateTimestamp(final Context context, String id, Uri[] folders){ 3840 int updated = 0; 3841 final long now = System.currentTimeMillis(); 3842 final ContentResolver resolver = context.getContentResolver(); 3843 final ContentValues touchValues = new ContentValues(); 3844 for (int i=0, size=folders.length; i < size; ++i) { 3845 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 3846 LogUtils.d(TAG, "updateStamp: %s updated", folders[i]); 3847 updated += resolver.update(folders[i], touchValues, null, null); 3848 } 3849 final Uri toNotify = 3850 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3851 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 3852 resolver.notifyChange(toNotify, null); 3853 return updated; 3854 } 3855 3856 /** 3857 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 3858 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 3859 * @param uri 3860 * @param values 3861 * @return nonzero value always. 3862 */ 3863 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 3864 final int numFolders = values.size(); 3865 final String id = uri.getPathSegments().get(1); 3866 final Uri[] folders = new Uri[numFolders]; 3867 final Context context = getContext(); 3868 final NotificationController controller = NotificationController.getInstance(context); 3869 int i = 0; 3870 for (final String uriString: values.keySet()) { 3871 folders[i] = Uri.parse(uriString); 3872 try { 3873 final String mailboxIdString = folders[i].getLastPathSegment(); 3874 final long mailboxId = Long.parseLong(mailboxIdString); 3875 controller.cancelNewMessageNotification(mailboxId); 3876 } catch (NumberFormatException e) { 3877 // Keep on going... 3878 } 3879 } 3880 return updateTimestamp(context, id, folders); 3881 } 3882 3883 /** 3884 * Populates the recent folders according to the design. 3885 * @param uri 3886 * @return the number of recent folders were populated. 3887 */ 3888 private int uiPopulateRecentFolders(Uri uri) { 3889 final Context context = getContext(); 3890 final String id = uri.getLastPathSegment(); 3891 final Uri[] recentFolders = defaultRecentFolders(id); 3892 final int numFolders = recentFolders.length; 3893 if (numFolders <= 0) { 3894 return 0; 3895 } 3896 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 3897 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 3898 return rowsUpdated; 3899 } 3900 3901 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 3902 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 3903 if (stateValue != null) { 3904 // This is a command from UIProvider 3905 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 3906 Context context = getContext(); 3907 Attachment attachment = 3908 Attachment.restoreAttachmentWithId(context, attachmentId); 3909 if (attachment == null) { 3910 // Went away; ah, well... 3911 return 0; 3912 } 3913 ContentValues values = new ContentValues(); 3914 switch (stateValue.intValue()) { 3915 case UIProvider.AttachmentState.NOT_SAVED: 3916 // Set state, try to cancel request 3917 values.put(AttachmentColumns.UI_STATE, stateValue); 3918 values.put(AttachmentColumns.FLAGS, 3919 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3920 attachment.update(context, values); 3921 return 1; 3922 case UIProvider.AttachmentState.DOWNLOADING: 3923 // Set state and destination; request download 3924 values.put(AttachmentColumns.UI_STATE, stateValue); 3925 Integer destinationValue = 3926 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 3927 values.put(AttachmentColumns.UI_DESTINATION, 3928 destinationValue == null ? 0 : destinationValue); 3929 values.put(AttachmentColumns.FLAGS, 3930 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3931 attachment.update(context, values); 3932 return 1; 3933 case UIProvider.AttachmentState.SAVED: 3934 // If this is an inline attachment, notify message has changed 3935 if (!TextUtils.isEmpty(attachment.mContentId)) { 3936 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 3937 } 3938 return 1; 3939 } 3940 } 3941 return 0; 3942 } 3943 3944 private int uiUpdateFolder(Uri uri, ContentValues uiValues) { 3945 Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 3946 if (ourUri == null) return 0; 3947 ContentValues ourValues = new ContentValues(); 3948 // This should only be called via update to "recent folders" 3949 for (String columnName: uiValues.keySet()) { 3950 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 3951 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 3952 } 3953 } 3954 return update(ourUri, ourValues, null, null); 3955 } 3956 3957 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 3958 ContentValues ourValues = new ContentValues(); 3959 for (String columnName : values.keySet()) { 3960 Object val = values.get(columnName); 3961 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 3962 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 3963 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 3964 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 3965 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3966 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 3967 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 3968 // Convert from folder list uri to mailbox key 3969 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName)); 3970 if (flist.folders.size() != 1) { 3971 LogUtils.e(TAG, 3972 "Incorrect number of folders for this message: Message is %s", 3973 message.mId); 3974 } else { 3975 final Folder f = flist.folders.get(0); 3976 final Uri uri = f.uri; 3977 final Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 3978 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 3979 } 3980 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 3981 Address[] fromList = Address.unpack(message.mFrom); 3982 Preferences prefs = Preferences.getPreferences(getContext()); 3983 for (Address sender : fromList) { 3984 String email = sender.getAddress(); 3985 prefs.setSenderAsTrusted(email); 3986 } 3987 } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) || 3988 columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) { 3989 // Ignore for now 3990 } else { 3991 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 3992 } 3993 } 3994 return ourValues; 3995 } 3996 3997 private Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 3998 String idString = uri.getLastPathSegment(); 3999 try { 4000 long id = Long.parseLong(idString); 4001 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 4002 if (asProvider) { 4003 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 4004 } 4005 return ourUri; 4006 } catch (NumberFormatException e) { 4007 return null; 4008 } 4009 } 4010 4011 private Message getMessageFromLastSegment(Uri uri) { 4012 long messageId = Long.parseLong(uri.getLastPathSegment()); 4013 return Message.restoreMessageWithId(getContext(), messageId); 4014 } 4015 4016 /** 4017 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 4018 * clear out the undo list and start over 4019 * @param uri the uri we're working on 4020 * @param op the ContentProviderOperation to perform upon undo 4021 */ 4022 private void addToSequence(Uri uri, ContentProviderOperation op) { 4023 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 4024 if (sequenceString != null) { 4025 int sequence = Integer.parseInt(sequenceString); 4026 if (sequence > mLastSequence) { 4027 // Reset sequence 4028 mLastSequenceOps.clear(); 4029 mLastSequence = sequence; 4030 } 4031 // TODO: Need something to indicate a change isn't ready (undoable) 4032 mLastSequenceOps.add(op); 4033 } 4034 } 4035 4036 // TODO: This should depend on flags on the mailbox... 4037 private boolean uploadsToServer(Context context, Mailbox m) { 4038 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 4039 m.mType == Mailbox.TYPE_SEARCH) { 4040 return false; 4041 } 4042 String protocol = Account.getProtocol(context, m.mAccountKey); 4043 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 4044 return (info != null && info.syncChanges); 4045 } 4046 4047 private int uiUpdateMessage(Uri uri, ContentValues values) { 4048 return uiUpdateMessage(uri, values, false); 4049 } 4050 4051 private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) { 4052 Context context = getContext(); 4053 Message msg = getMessageFromLastSegment(uri); 4054 if (msg == null) return 0; 4055 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4056 if (mailbox == null) return 0; 4057 Uri ourBaseUri = 4058 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI : 4059 Message.CONTENT_URI; 4060 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 4061 if (ourUri == null) return 0; 4062 4063 // Special case - meeting response 4064 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 4065 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 4066 mServiceCallback, mailbox.mAccountKey); 4067 try { 4068 service.sendMeetingResponse(msg.mId, 4069 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 4070 // Delete the message immediately 4071 uiDeleteMessage(uri); 4072 Utility.showToast(context, R.string.confirm_response); 4073 // Notify box has changed so the deletion is reflected in the UI 4074 notifyUIConversationMailbox(mailbox.mId); 4075 } catch (RemoteException e) { 4076 } 4077 return 1; 4078 } 4079 4080 ContentValues undoValues = new ContentValues(); 4081 ContentValues ourValues = convertUiMessageValues(msg, values); 4082 for (String columnName: ourValues.keySet()) { 4083 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4084 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 4085 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 4086 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 4087 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 4088 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 4089 } 4090 } 4091 if (undoValues == null || undoValues.size() == 0) { 4092 return -1; 4093 } 4094 final Boolean suppressUndo = 4095 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO); 4096 if (suppressUndo == null || !suppressUndo.booleanValue()) { 4097 final ContentProviderOperation op = 4098 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 4099 uri, ourBaseUri, false)) 4100 .withValues(undoValues) 4101 .build(); 4102 addToSequence(uri, op); 4103 } 4104 return update(ourUri, ourValues, null, null); 4105 } 4106 4107 public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; 4108 public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; 4109 public static final String PICKER_MESSAGE_ID = "picker_message_id"; 4110 public static final String PICKER_HEADER_ID = "picker_header_id"; 4111 4112 private int uiDeleteMessage(Uri uri) { 4113 final Context context = getContext(); 4114 Message msg = getMessageFromLastSegment(uri); 4115 if (msg == null) return 0; 4116 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4117 if (mailbox == null) return 0; 4118 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 4119 // We actually delete these, including attachments 4120 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 4121 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 4122 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, mailbox.mAccountKey); 4123 return context.getContentResolver().delete( 4124 ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); 4125 } 4126 Mailbox trashMailbox = 4127 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 4128 if (trashMailbox == null) { 4129 return 0; 4130 } 4131 ContentValues values = new ContentValues(); 4132 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 4133 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, mailbox.mId); 4134 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, mailbox.mId); 4135 return uiUpdateMessage(uri, values, true); 4136 } 4137 4138 private int pickFolder(Uri uri, int type, int headerId) { 4139 Context context = getContext(); 4140 Long acctId = Long.parseLong(uri.getLastPathSegment()); 4141 // For push imap, for example, we want the user to select the trash mailbox 4142 Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, 4143 null, null, null); 4144 try { 4145 if (ac.moveToFirst()) { 4146 final com.android.mail.providers.Account uiAccount = 4147 new com.android.mail.providers.Account(ac); 4148 Intent intent = new Intent(context, FolderPickerActivity.class); 4149 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); 4150 intent.putExtra(PICKER_MAILBOX_TYPE, type); 4151 intent.putExtra(PICKER_HEADER_ID, headerId); 4152 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 4153 context.startActivity(intent); 4154 return 1; 4155 } 4156 return 0; 4157 } finally { 4158 ac.close(); 4159 } 4160 } 4161 4162 private int pickTrashFolder(Uri uri) { 4163 return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title); 4164 } 4165 4166 private int pickSentFolder(Uri uri) { 4167 return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title); 4168 } 4169 4170 private Cursor uiUndo(String[] projection) { 4171 // First see if we have any operations saved 4172 // TODO: Make sure seq matches 4173 if (!mLastSequenceOps.isEmpty()) { 4174 try { 4175 // TODO Always use this projection? Or what's passed in? 4176 // Not sure if UI wants it, but I'm making a cursor of convo uri's 4177 MatrixCursor c = new MatrixCursorWithCachedColumns( 4178 new String[] {UIProvider.ConversationColumns.URI}, 4179 mLastSequenceOps.size()); 4180 for (ContentProviderOperation op: mLastSequenceOps) { 4181 c.addRow(new String[] {op.getUri().toString()}); 4182 } 4183 // Just apply the batch and we're done! 4184 applyBatch(mLastSequenceOps); 4185 // But clear the operations 4186 mLastSequenceOps.clear(); 4187 // Tell the UI there are changes 4188 ContentResolver resolver = getContext().getContentResolver(); 4189 resolver.notifyChange(UIPROVIDER_CONVERSATION_NOTIFIER, null); 4190 resolver.notifyChange(UIPROVIDER_FOLDER_NOTIFIER, null); 4191 resolver.notifyChange(UIPROVIDER_FOLDERLIST_NOTIFIER, null); 4192 return c; 4193 } catch (OperationApplicationException e) { 4194 } 4195 } 4196 return new MatrixCursorWithCachedColumns(projection, 0); 4197 } 4198 4199 private void notifyUIConversation(Uri uri) { 4200 String id = uri.getLastPathSegment(); 4201 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 4202 if (msg != null) { 4203 notifyUIConversationMailbox(msg.mMailboxKey); 4204 } 4205 } 4206 4207 /** 4208 * Notify about the Mailbox id passed in 4209 * @param id the Mailbox id to be notified 4210 */ 4211 private void notifyUIConversationMailbox(long id) { 4212 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 4213 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 4214 if (mailbox == null) { 4215 Log.w(TAG, "No mailbox for notification: " + id); 4216 return; 4217 } 4218 // Notify combined inbox... 4219 if (mailbox.mType == Mailbox.TYPE_INBOX) { 4220 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 4221 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 4222 } 4223 notifyWidgets(id); 4224 } 4225 4226 private void notifyUI(Uri uri, String id) { 4227 Uri notifyUri = uri.buildUpon().appendPath(id).build(); 4228 getContext().getContentResolver().notifyChange(notifyUri, null); 4229 } 4230 4231 private void notifyUI(Uri uri, long id) { 4232 notifyUI(uri, Long.toString(id)); 4233 } 4234 4235 /** 4236 * Support for services and service notifications 4237 */ 4238 4239 private final IEmailServiceCallback.Stub mServiceCallback = 4240 new IEmailServiceCallback.Stub() { 4241 4242 @Override 4243 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 4244 throws RemoteException { 4245 } 4246 4247 @Override 4248 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 4249 throws RemoteException { 4250 // We'll get callbacks here from the services, which we'll pass back to the UI 4251 Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, mailboxId); 4252 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 4253 } 4254 4255 @Override 4256 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 4257 int progress) throws RemoteException { 4258 } 4259 4260 @Override 4261 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 4262 throws RemoteException { 4263 } 4264 4265 @Override 4266 public void loadMessageStatus(long messageId, int statusCode, int progress) 4267 throws RemoteException { 4268 } 4269 }; 4270 4271 private Cursor uiFolderRefresh(Uri uri) { 4272 Context context = getContext(); 4273 String idString = uri.getLastPathSegment(); 4274 long id = Long.parseLong(idString); 4275 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 4276 if (mailbox == null) return null; 4277 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 4278 mServiceCallback, mailbox.mAccountKey); 4279 try { 4280 service.startSync(id, true); 4281 } catch (RemoteException e) { 4282 } 4283 return null; 4284 } 4285 4286 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 4287 public static final int VISIBLE_LIMIT_INCREMENT = 10; 4288 //Number of additional messages to load when a user selects "Load more..." in a search 4289 public static final int SEARCH_MORE_INCREMENT = 10; 4290 4291 private Cursor uiFolderLoadMore(Uri uri) { 4292 Context context = getContext(); 4293 String idString = uri.getLastPathSegment(); 4294 long id = Long.parseLong(idString); 4295 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, id); 4296 if (mailbox == null) return null; 4297 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 4298 // Ask for 10 more messages 4299 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 4300 runSearchQuery(context, mailbox.mAccountKey, id); 4301 } else { 4302 ContentValues values = new ContentValues(); 4303 values.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 4304 values.put(EmailContent.ADD_COLUMN_NAME, VISIBLE_LIMIT_INCREMENT); 4305 Uri mailboxUri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, id); 4306 // Increase the limit 4307 context.getContentResolver().update(mailboxUri, values, null, null); 4308 // And order a refresh 4309 uiFolderRefresh(uri); 4310 } 4311 return null; 4312 } 4313 4314 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 4315 private SearchParams mSearchParams; 4316 4317 /** 4318 * Returns the search mailbox for the specified account, creating one if necessary 4319 * @return the search mailbox for the passed in account 4320 */ 4321 private Mailbox getSearchMailbox(long accountId) { 4322 Context context = getContext(); 4323 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 4324 if (m == null) { 4325 m = new Mailbox(); 4326 m.mAccountKey = accountId; 4327 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 4328 m.mFlagVisible = false; 4329 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 4330 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 4331 m.mType = Mailbox.TYPE_SEARCH; 4332 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 4333 m.mParentKey = Mailbox.NO_MAILBOX; 4334 m.save(context); 4335 } 4336 return m; 4337 } 4338 4339 private void runSearchQuery(final Context context, final long accountId, 4340 final long searchMailboxId) { 4341 // Start the search running in the background 4342 new Thread(new Runnable() { 4343 @Override 4344 public void run() { 4345 try { 4346 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(context, 4347 mServiceCallback, accountId); 4348 if (service != null) { 4349 try { 4350 // Save away the total count 4351 mSearchParams.mTotalCount = service.searchMessages(accountId, 4352 mSearchParams, searchMailboxId); 4353 //Log.d(TAG, "TotalCount to UI: " + mSearchParams.mTotalCount); 4354 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, searchMailboxId); 4355 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 4356 } catch (RemoteException e) { 4357 Log.e("searchMessages", "RemoteException", e); 4358 } 4359 } 4360 } finally { 4361 } 4362 }}).start(); 4363 4364 } 4365 4366 // TODO: Handle searching for more... 4367 private Cursor uiSearch(Uri uri, String[] projection) { 4368 final long accountId = Long.parseLong(uri.getLastPathSegment()); 4369 4370 // TODO: Check the actual mailbox 4371 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 4372 if (inbox == null) { 4373 Log.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account " + accountId); 4374 return null; 4375 } 4376 4377 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 4378 if (filter == null) { 4379 throw new IllegalArgumentException("No query parameter in search query"); 4380 } 4381 4382 // Find/create our search mailbox 4383 Mailbox searchMailbox = getSearchMailbox(accountId); 4384 final long searchMailboxId = searchMailbox.mId; 4385 4386 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 4387 4388 final Context context = getContext(); 4389 if (mSearchParams.mOffset == 0) { 4390 // Delete existing contents of search mailbox 4391 ContentResolver resolver = context.getContentResolver(); 4392 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 4393 null); 4394 ContentValues cv = new ContentValues(); 4395 // For now, use the actual query as the name of the mailbox 4396 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 4397 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 4398 cv, null, null); 4399 } 4400 4401 // Start the search running in the background 4402 runSearchQuery(context, accountId, searchMailboxId); 4403 4404 // This will look just like a "normal" folder 4405 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 4406 searchMailbox.mId), projection); 4407 } 4408 4409 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 4410 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 4411 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 4412 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 4413 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 4414 4415 /** 4416 * Delete an account and clean it up 4417 */ 4418 private int uiDeleteAccount(Uri uri) { 4419 Context context = getContext(); 4420 long accountId = Long.parseLong(uri.getLastPathSegment()); 4421 try { 4422 // Get the account URI. 4423 final Account account = Account.restoreAccountWithId(context, accountId); 4424 if (account == null) { 4425 return 0; // Already deleted? 4426 } 4427 4428 deleteAccountData(context, accountId); 4429 4430 // Now delete the account itself 4431 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 4432 context.getContentResolver().delete(uri, null, null); 4433 4434 // Clean up 4435 AccountBackupRestore.backup(context); 4436 SecurityPolicy.getInstance(context).reducePolicies(); 4437 MailActivityEmail.setServicesEnabledSync(context); 4438 return 1; 4439 } catch (Exception e) { 4440 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 4441 } 4442 return 0; 4443 } 4444 4445 private int uiDeleteAccountData(Uri uri) { 4446 Context context = getContext(); 4447 long accountId = Long.parseLong(uri.getLastPathSegment()); 4448 // Get the account URI. 4449 final Account account = Account.restoreAccountWithId(context, accountId); 4450 if (account == null) { 4451 return 0; // Already deleted? 4452 } 4453 deleteAccountData(context, accountId); 4454 return 1; 4455 } 4456 4457 private void deleteAccountData(Context context, long accountId) { 4458 // Delete synced attachments 4459 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 4460 4461 // Delete synced email, leaving only an empty inbox. We do this in two phases: 4462 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 4463 // 2. Delete all remaining messages (which will be the inbox messages) 4464 ContentResolver resolver = context.getContentResolver(); 4465 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 4466 resolver.delete(Mailbox.CONTENT_URI, 4467 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 4468 accountIdArgs); 4469 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4470 4471 // Delete sync keys on remaining items 4472 ContentValues cv = new ContentValues(); 4473 cv.putNull(Account.SYNC_KEY); 4474 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 4475 cv.clear(); 4476 cv.putNull(Mailbox.SYNC_KEY); 4477 resolver.update(Mailbox.CONTENT_URI, cv, 4478 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4479 4480 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 4481 IEmailService service = EmailServiceUtils.getServiceForAccount(context, null, accountId); 4482 if (service != null) { 4483 try { 4484 service.deleteAccountPIMData(accountId); 4485 } catch (RemoteException e) { 4486 // Can't do anything about this 4487 } 4488 } 4489 } 4490 4491 private int[] mSavedWidgetIds = new int[0]; 4492 private ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 4493 private AppWidgetManager mAppWidgetManager; 4494 private ComponentName mEmailComponent; 4495 4496 private void notifyWidgets(long mailboxId) { 4497 Context context = getContext(); 4498 // Lazily initialize these 4499 if (mAppWidgetManager == null) { 4500 mAppWidgetManager = AppWidgetManager.getInstance(context); 4501 mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME); 4502 } 4503 4504 // See if we have to populate our array of mailboxes used in widgets 4505 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 4506 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 4507 mSavedWidgetIds = widgetIds; 4508 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 4509 // widgetInfo now has pairs of account uri/folder uri 4510 mWidgetNotifyMailboxes.clear(); 4511 for (String[] widgetInfo: widgetInfos) { 4512 try { 4513 if (widgetInfo == null) continue; 4514 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 4515 if (!isCombinedMailbox(id)) { 4516 // For a regular mailbox, just add it to the list 4517 if (!mWidgetNotifyMailboxes.contains(id)) { 4518 mWidgetNotifyMailboxes.add(id); 4519 } 4520 } else { 4521 switch (getVirtualMailboxType(id)) { 4522 // We only handle the combined inbox in widgets 4523 case Mailbox.TYPE_INBOX: 4524 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 4525 MailboxColumns.TYPE + "=?", 4526 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 4527 try { 4528 while (c.moveToNext()) { 4529 mWidgetNotifyMailboxes.add( 4530 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 4531 } 4532 } finally { 4533 c.close(); 4534 } 4535 break; 4536 } 4537 } 4538 } catch (NumberFormatException e) { 4539 // Move along 4540 } 4541 } 4542 } 4543 4544 // If our mailbox needs to be notified, do so... 4545 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 4546 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 4547 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 4548 intent.setType(EMAIL_APP_MIME_TYPE); 4549 context.sendBroadcast(intent); 4550 } 4551 } 4552 4553 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 4554 Context context = getContext(); 4555 writer.println("Installed services:"); 4556 for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) { 4557 writer.println(" " + info); 4558 } 4559 writer.println(); 4560 writer.println("Accounts: "); 4561 Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); 4562 if (cursor.getCount() == 0) { 4563 writer.println(" None"); 4564 } 4565 try { 4566 while (cursor.moveToNext()) { 4567 Account account = new Account(); 4568 account.restore(cursor); 4569 writer.println(" Account " + account.mDisplayName); 4570 HostAuth hostAuth = 4571 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 4572 if (hostAuth != null) { 4573 writer.println(" Protocol = " + hostAuth.mProtocol + 4574 (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " + 4575 account.mProtocolVersion)); 4576 } 4577 } 4578 } finally { 4579 cursor.close(); 4580 } 4581 } 4582} 4583