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