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