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