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