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