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