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