EmailProvider.java revision 9e521deb6bb525b33365cc2926cb2d0faa7095e2
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 private static ProjectionMap getFolderListMap() { 1937 if (sFolderListMap == null) { 1938 sFolderListMap = ProjectionMap.builder() 1939 .add(BaseColumns._ID, MailboxColumns.ID) 1940 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) 1941 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 1942 .add(UIProvider.FolderColumns.NAME, "displayName") 1943 .add(UIProvider.FolderColumns.HAS_CHILDREN, 1944 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 1945 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 1946 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 1947 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 1948 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 1949 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 1950 .add(UIProvider.FolderColumns.TOTAL_COUNT, MailboxColumns.TOTAL_COUNT) 1951 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH)) 1952 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 1953 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 1954 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 1955 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 1956 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 1957 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY 1958 + "=" + Mailbox.NO_MAILBOX + " then NULL else " + 1959 uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end") 1960 .build(); 1961 } 1962 return sFolderListMap; 1963 } 1964 private static ProjectionMap sFolderListMap; 1965 1966 /** 1967 * Constructs the map of default entries for accounts. These values can be overridden in 1968 * {@link #genQueryAccount(String[], String)}. 1969 */ 1970 private static ProjectionMap getAccountListMap(Context context) { 1971 if (sAccountListMap == null) { 1972 final MailPrefs mailPrefs = MailPrefs.get(context); 1973 1974 final ProjectionMap.Builder builder = ProjectionMap.builder() 1975 .add(BaseColumns._ID, AccountColumns.ID) 1976 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 1977 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 1978 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 1979 .add(UIProvider.AccountColumns.UNDO_URI, 1980 ("'content://" + EmailContent.AUTHORITY + "/uiundo'")) 1981 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 1982 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 1983 // TODO: Is provider version used? 1984 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 1985 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 1986 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, 1987 uriWithId("uirecentfolders")) 1988 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 1989 uriWithId("uidefaultrecentfolders")) 1990 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, 1991 AccountColumns.SIGNATURE) 1992 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 1993 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 1994 .add(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, 1995 Integer.toString(mailPrefs.getDefaultReplyAll() 1996 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 1997 : UIProvider.DefaultReplyBehavior.REPLY)) 1998 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 1999 .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, 2000 Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) 2001 .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null); 2002 2003 final String feedbackUri = context.getString(R.string.email_feedback_uri); 2004 if (!TextUtils.isEmpty(feedbackUri)) { 2005 // This string needs to be in single quotes, as it will be used as a constant 2006 // in a sql expression 2007 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, 2008 "'" + feedbackUri + "'"); 2009 } 2010 2011 sAccountListMap = builder.build(); 2012 } 2013 return sAccountListMap; 2014 } 2015 private static ProjectionMap sAccountListMap; 2016 2017 /** 2018 * The "ORDER BY" clause for top level folders 2019 */ 2020 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2021 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2022 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2023 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2024 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2025 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2026 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2027 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2028 + " ELSE 10 END" 2029 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2030 2031 /** 2032 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2033 */ 2034 private static ProjectionMap getAttachmentMap() { 2035 if (sAttachmentMap == null) { 2036 sAttachmentMap = ProjectionMap.builder() 2037 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2038 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2039 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2040 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2041 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2042 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2043 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, 2044 AttachmentColumns.UI_DOWNLOADED_SIZE) 2045 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2046 .build(); 2047 } 2048 return sAttachmentMap; 2049 } 2050 private static ProjectionMap sAttachmentMap; 2051 2052 /** 2053 * Generate the SELECT clause using a specified mapping and the original UI projection 2054 * @param map the ProjectionMap to use for this projection 2055 * @param projection the projection as sent by UnifiedEmail 2056 * @return a StringBuilder containing the SELECT expression for a SQLite query 2057 */ 2058 private static StringBuilder genSelect(ProjectionMap map, String[] projection) { 2059 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2060 } 2061 2062 private static StringBuilder genSelect(ProjectionMap map, String[] projection, 2063 ContentValues values) { 2064 StringBuilder sb = new StringBuilder("SELECT "); 2065 boolean first = true; 2066 for (String column: projection) { 2067 if (first) { 2068 first = false; 2069 } else { 2070 sb.append(','); 2071 } 2072 String val = null; 2073 // First look at values; this is an override of default behavior 2074 if (values.containsKey(column)) { 2075 String value = values.getAsString(column); 2076 if (value == null) { 2077 val = "NULL AS " + column; 2078 } else if (value.startsWith("@")) { 2079 val = value.substring(1) + " AS " + column; 2080 } else { 2081 val = "'" + value + "' AS " + column; 2082 } 2083 } else { 2084 // Now, get the standard value for the column from our projection map 2085 val = map.get(column); 2086 // If we don't have the column, return "NULL AS <column>", and warn 2087 if (val == null) { 2088 val = "NULL AS " + column; 2089 } 2090 } 2091 sb.append(val); 2092 } 2093 return sb; 2094 } 2095 2096 /** 2097 * Convenience method to create a Uri string given the "type" of query; we append the type 2098 * of the query and the id column name (_id) 2099 * 2100 * @param type the "type" of the query, as defined by our UriMatcher definitions 2101 * @return a Uri string 2102 */ 2103 private static String uriWithId(String type) { 2104 return uriWithColumn(type, EmailContent.RECORD_ID); 2105 } 2106 2107 /** 2108 * Convenience method to create a Uri string given the "type" of query; we append the type 2109 * of the query and the passed in column name 2110 * 2111 * @param type the "type" of the query, as defined by our UriMatcher definitions 2112 * @param columnName the column in the table being queried 2113 * @return a Uri string 2114 */ 2115 private static String uriWithColumn(String type, String columnName) { 2116 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 2117 } 2118 2119 /** 2120 * Convenience method to create a Uri string given the "type" of query and the table name to 2121 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2122 * (i.e. including the table name); we need this for join queries where _id would otherwise 2123 * be ambiguous 2124 * 2125 * @param type the "type" of the query, as defined by our UriMatcher definitions 2126 * @param tableName the name of the table whose _id is referred to 2127 * @return a Uri string 2128 */ 2129 private static String uriWithFQId(String type, String tableName) { 2130 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2131 } 2132 2133 // Regex that matches start of img tag. '<(?i)img\s+'. 2134 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 2135 2136 /** 2137 * Class that holds the sqlite query and the attachment (JSON) value (which might be null) 2138 */ 2139 private static class MessageQuery { 2140 final String query; 2141 final String attachmentJson; 2142 2143 MessageQuery(String _query, String _attachmentJson) { 2144 query = _query; 2145 attachmentJson = _attachmentJson; 2146 } 2147 } 2148 2149 /** 2150 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2151 * 2152 * @param uiProjection as passed from UnifiedEmail 2153 * @return the SQLite query to be executed on the EmailProvider database 2154 */ 2155 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 2156 Context context = getContext(); 2157 long messageId = Long.parseLong(id); 2158 Message msg = Message.restoreMessageWithId(context, messageId); 2159 ContentValues values = new ContentValues(); 2160 String attachmentJson = null; 2161 if (msg != null) { 2162 Body body = Body.restoreBodyWithMessageId(context, messageId); 2163 if (body != null) { 2164 if (body.mHtmlContent != null) { 2165 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2166 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2167 } 2168 } 2169 } 2170 Address[] fromList = Address.unpack(msg.mFrom); 2171 int autoShowImages = 0; 2172 final MailPrefs mailPrefs = MailPrefs.get(context); 2173 for (Address sender : fromList) { 2174 final String email = sender.getAddress(); 2175 if (mailPrefs.getDisplayImagesFromSender(email)) { 2176 autoShowImages = 1; 2177 break; 2178 } 2179 } 2180 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2181 // Add attachments... 2182 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2183 if (atts.length > 0) { 2184 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2185 new ArrayList<com.android.mail.providers.Attachment>(); 2186 for (Attachment att : atts) { 2187 if (att.mContentId != null && att.getContentUri() != null) { 2188 continue; 2189 } 2190 com.android.mail.providers.Attachment uiAtt = 2191 new com.android.mail.providers.Attachment(); 2192 uiAtt.setName(att.mFileName); 2193 uiAtt.setContentType(att.mMimeType); 2194 uiAtt.size = (int) att.mSize; 2195 uiAtt.uri = uiUri("uiattachment", att.mId); 2196 uiAtts.add(uiAtt); 2197 } 2198 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 2199 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 2200 } 2201 if (msg.mDraftInfo != 0) { 2202 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2203 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2204 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2205 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2206 } 2207 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2208 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2209 "content://ui.email2.android.com/event/" + msg.mId); 2210 } 2211 } 2212 StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); 2213 sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + " ON " + 2214 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " WHERE " + 2215 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2216 String sql = sb.toString(); 2217 return new MessageQuery(sql, attachmentJson); 2218 } 2219 2220 /** 2221 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2222 * 2223 * @param uiProjection as passed from UnifiedEmail 2224 * @param unseenOnly <code>true</code> to only return unseen messages 2225 * @return the SQLite query to be executed on the EmailProvider database 2226 */ 2227 private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { 2228 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2229 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2230 Message.FLAG_LOADED_SELECTION + " AND " + 2231 Message.MAILBOX_KEY + "=? "); 2232 if (unseenOnly) { 2233 sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); 2234 } 2235 sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC "); 2236 sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT); 2237 return sb.toString(); 2238 } 2239 2240 /** 2241 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2242 * 2243 * @param uiProjection as passed from UnifiedEmail 2244 * @param mailboxId the id of the virtual mailbox 2245 * @param unseenOnly <code>true</code> to only return unseen messages 2246 * @return the SQLite query to be executed on the EmailProvider database 2247 */ 2248 private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2249 long mailboxId, final boolean unseenOnly) { 2250 ContentValues values = new ContentValues(); 2251 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2252 final int virtualMailboxId = getVirtualMailboxType(mailboxId); 2253 final String[] selectionArgs; 2254 StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); 2255 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2256 Message.FLAG_LOADED_SELECTION + " AND "); 2257 if (isCombinedMailbox(mailboxId)) { 2258 if (unseenOnly) { 2259 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND "); 2260 } 2261 selectionArgs = null; 2262 } else { 2263 if (virtualMailboxId == Mailbox.TYPE_INBOX) { 2264 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2265 } 2266 sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND "); 2267 selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)}; 2268 } 2269 switch (getVirtualMailboxType(mailboxId)) { 2270 case Mailbox.TYPE_INBOX: 2271 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2272 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2273 "=" + Mailbox.TYPE_INBOX + ")"); 2274 break; 2275 case Mailbox.TYPE_STARRED: 2276 sb.append(MessageColumns.FLAG_FAVORITE + "=1"); 2277 break; 2278 case Mailbox.TYPE_UNREAD: 2279 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY + 2280 " NOT IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + 2281 " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")"); 2282 break; 2283 default: 2284 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2285 } 2286 sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); 2287 return db.rawQuery(sb.toString(), selectionArgs); 2288 } 2289 2290 /** 2291 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2292 * 2293 * @param uiProjection as passed from UnifiedEmail 2294 * @return the SQLite query to be executed on the EmailProvider database 2295 */ 2296 private static String genQueryConversation(String[] uiProjection) { 2297 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2298 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2299 return sb.toString(); 2300 } 2301 2302 /** 2303 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2304 * 2305 * @param uiProjection as passed from UnifiedEmail 2306 * @return the SQLite query to be executed on the EmailProvider database 2307 */ 2308 private static String genQueryAccountMailboxes(String[] uiProjection) { 2309 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2310 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2311 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2312 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2313 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2314 sb.append(MAILBOX_ORDER_BY); 2315 return sb.toString(); 2316 } 2317 2318 /** 2319 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2320 * sorted by the name as it appears in a hierarchical listing 2321 * 2322 * @param uiProjection as passed from UnifiedEmail 2323 * @return the SQLite query to be executed on the EmailProvider database 2324 */ 2325 private static String genQueryAccountAllMailboxes(String[] uiProjection) { 2326 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2327 // Use a derived column to choose either hierarchicalName or displayName 2328 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2329 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2330 " end as h_name"); 2331 // Order by the derived column 2332 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2333 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2334 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2335 " ORDER BY h_name"); 2336 return sb.toString(); 2337 } 2338 2339 /** 2340 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2341 * 2342 * @param uiProjection as passed from UnifiedEmail 2343 * @return the SQLite query to be executed on the EmailProvider database 2344 */ 2345 private static String genQueryRecentMailboxes(String[] uiProjection) { 2346 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2347 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2348 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2349 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2350 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2351 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2352 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2353 return sb.toString(); 2354 } 2355 2356 private static int getFolderCapabilities(EmailServiceInfo info, int flags, int type, 2357 long mailboxId) { 2358 // All folders support delete 2359 int caps = UIProvider.FolderCapabilities.DELETE; 2360 if (info != null && info.offerLookback) { 2361 // Protocols supporting lookback support settings 2362 caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2363 } 2364 if ((flags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0) { 2365 // If the mailbox can accept moved mail, report that as well 2366 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2367 caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION; 2368 } 2369 2370 // For trash, we don't allow undo 2371 if (type == Mailbox.TYPE_TRASH) { 2372 caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2373 UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION | 2374 UIProvider.FolderCapabilities.CAN_HOLD_MAIL | 2375 UIProvider.FolderCapabilities.DELETE | 2376 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; 2377 } 2378 if (isVirtualMailbox(mailboxId)) { 2379 caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; 2380 } 2381 return caps; 2382 } 2383 2384 /** 2385 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2386 * 2387 * @param uiProjection as passed from UnifiedEmail 2388 * @return the SQLite query to be executed on the EmailProvider database 2389 */ 2390 private String genQueryMailbox(String[] uiProjection, String id) { 2391 long mailboxId = Long.parseLong(id); 2392 ContentValues values = new ContentValues(); 2393 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2394 // This is the current search mailbox; use the total count 2395 values = new ContentValues(); 2396 values.put(UIProvider.FolderColumns.TOTAL_COUNT, mSearchParams.mTotalCount); 2397 // "load more" is valid for search results 2398 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2399 uiUriString("uiloadmore", mailboxId)); 2400 values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE); 2401 } else { 2402 Context context = getContext(); 2403 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2404 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2405 if (mailbox != null) { 2406 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2407 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2408 // All folders support delete 2409 if (info != null && info.offerLoadMore) { 2410 // "load more" is valid for protocols not supporting "lookback" 2411 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2412 uiUriString("uiloadmore", mailboxId)); 2413 } 2414 values.put(UIProvider.FolderColumns.CAPABILITIES, 2415 getFolderCapabilities(info, mailbox.mFlags, mailbox.mType, mailboxId)); 2416 // The persistent id is used to form a filename, so we must ensure that it doesn't 2417 // include illegal characters (such as '/'). Only perform the encoding if this 2418 // query wants the persistent id. 2419 boolean shouldEncodePersistentId = false; 2420 if (uiProjection == null) { 2421 shouldEncodePersistentId = true; 2422 } else { 2423 for (final String column : uiProjection) { 2424 if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) { 2425 shouldEncodePersistentId = true; 2426 break; 2427 } 2428 } 2429 } 2430 if (shouldEncodePersistentId) { 2431 values.put(UIProvider.FolderColumns.PERSISTENT_ID, 2432 Base64.encodeToString(mailbox.mServerId.getBytes(), 2433 Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); 2434 } 2435 } 2436 } 2437 StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); 2438 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2439 return sb.toString(); 2440 } 2441 2442 public static final String LEGACY_AUTHORITY = "ui.email.android.com"; 2443 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY); 2444 2445 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2446 2447 private static String getExternalUriString(String segment, String account) { 2448 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2449 .appendQueryParameter("account", account).build().toString(); 2450 } 2451 2452 private static String getExternalUriStringEmail2(String segment, String account) { 2453 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2454 .appendQueryParameter("account", account).build().toString(); 2455 } 2456 2457 private static String getBits(int bitField) { 2458 StringBuilder sb = new StringBuilder(" "); 2459 for (int i = 0; i < 32; i++, bitField >>= 1) { 2460 if ((bitField & 1) != 0) { 2461 sb.append("" + i + " "); 2462 } 2463 } 2464 return sb.toString(); 2465 } 2466 2467 private static int getCapabilities(Context context, long accountId) { 2468 final EmailServiceProxy service = 2469 EmailServiceUtils.getServiceForAccount(context, accountId); 2470 int capabilities = 0; 2471 Account acct = null; 2472 try { 2473 service.setTimeout(10); 2474 acct = Account.restoreAccountWithId(context, accountId); 2475 if (acct == null) { 2476 LogUtils.d(TAG, "getCapabilities() for " + accountId 2477 + ": returning 0x0 (no account)"); 2478 return 0; 2479 } 2480 capabilities = service.getCapabilities(acct); 2481 LogUtils.d(TAG, "getCapabilities() for %s: 0x%x %s", 2482 LogUtils.sanitizeAccountName(acct.mDisplayName), 2483 capabilities, getBits(capabilities)); 2484 } catch (RemoteException e) { 2485 // Nothing to do 2486 LogUtils.w(TAG, "getCapabilities() for " + acct.mDisplayName + ": RemoteException"); 2487 } 2488 2489 // If the configuration states that feedback is supported, add that capability 2490 final Resources res = context.getResources(); 2491 if (res.getBoolean(R.bool.feedback_supported)) { 2492 capabilities |= UIProvider.AccountCapabilities.SEND_FEEDBACK; 2493 } 2494 return capabilities; 2495 } 2496 2497 /** 2498 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2499 * 2500 * @param uiProjection as passed from UnifiedEmail 2501 * @return the SQLite query to be executed on the EmailProvider database 2502 */ 2503 private String genQueryAccount(String[] uiProjection, String id) { 2504 final ContentValues values = new ContentValues(); 2505 final long accountId = Long.parseLong(id); 2506 final Context context = getContext(); 2507 2508 EmailServiceInfo info = null; 2509 2510 // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null. 2511 final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection); 2512 2513 if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { 2514 // Get account capabilities from the service 2515 values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId)); 2516 } 2517 if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2518 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2519 getExternalUriString("settings", id)); 2520 } 2521 if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { 2522 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2523 getExternalUriStringEmail2("compose", id)); 2524 } 2525 if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { 2526 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2527 } 2528 if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { 2529 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2530 } 2531 2532 final Preferences prefs = Preferences.getPreferences(getContext()); 2533 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 2534 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 2535 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 2536 prefs.getConfirmDelete() ? "1" : "0"); 2537 } 2538 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 2539 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 2540 prefs.getConfirmSend() ? "1" : "0"); 2541 } 2542 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) { 2543 values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE, 2544 mailPrefs.getConversationListSwipeActionInteger(false)); 2545 } 2546 if (projectionColumns.contains( 2547 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 2548 values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON, 2549 getConversationListIcon(mailPrefs)); 2550 } 2551 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 2552 int autoAdvance = prefs.getAutoAdvanceDirection(); 2553 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 2554 autoAdvanceToUiValue(autoAdvance)); 2555 } 2556 if (projectionColumns.contains( 2557 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 2558 int textZoom = prefs.getTextZoom(); 2559 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 2560 textZoomToUiValue(textZoom)); 2561 } 2562 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 2563 final long inboxMailboxId = 2564 Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 2565 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && 2566 inboxMailboxId != Mailbox.NO_MAILBOX) { 2567 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 2568 uiUriString("uifolder", inboxMailboxId)); 2569 } 2570 if (projectionColumns.contains( 2571 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) && 2572 inboxMailboxId != Mailbox.NO_MAILBOX) { 2573 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, 2574 Mailbox.getDisplayName(context, inboxMailboxId)); 2575 } 2576 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) { 2577 if (inboxMailboxId != Mailbox.NO_MAILBOX) { 2578 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 2579 } else { 2580 values.put(UIProvider.AccountColumns.SYNC_STATUS, 2581 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 2582 } 2583 } 2584 if (projectionColumns.contains( 2585 UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) { 2586 // Email doesn't support priority inbox, so always state priority arrows disabled. 2587 values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0"); 2588 } 2589 if (projectionColumns.contains( 2590 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) { 2591 // Set the setup intent if needed 2592 // TODO We should clarify/document the trash/setup relationship 2593 long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH); 2594 if (trashId == Mailbox.NO_MAILBOX) { 2595 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 2596 if (info != null && info.requiresSetup) { 2597 values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI, 2598 getExternalUriString("setup", id)); 2599 } 2600 } 2601 } 2602 if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) { 2603 final String type; 2604 if (info == null) { 2605 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 2606 } 2607 if (info != null) { 2608 type = info.accountType; 2609 } else { 2610 type = "unknown"; 2611 } 2612 2613 values.put(UIProvider.AccountColumns.TYPE, type); 2614 } 2615 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) && 2616 inboxMailboxId != Mailbox.NO_MAILBOX) { 2617 values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX, 2618 uiUriString("uifolder", inboxMailboxId)); 2619 } 2620 2621 final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values); 2622 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 2623 return sb.toString(); 2624 } 2625 2626 private static int autoAdvanceToUiValue(int autoAdvance) { 2627 switch(autoAdvance) { 2628 case Preferences.AUTO_ADVANCE_OLDER: 2629 return UIProvider.AutoAdvance.OLDER; 2630 case Preferences.AUTO_ADVANCE_NEWER: 2631 return UIProvider.AutoAdvance.NEWER; 2632 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 2633 default: 2634 return UIProvider.AutoAdvance.LIST; 2635 } 2636 } 2637 2638 private static int textZoomToUiValue(int textZoom) { 2639 switch(textZoom) { 2640 case Preferences.TEXT_ZOOM_HUGE: 2641 return UIProvider.MessageTextSize.HUGE; 2642 case Preferences.TEXT_ZOOM_LARGE: 2643 return UIProvider.MessageTextSize.LARGE; 2644 case Preferences.TEXT_ZOOM_NORMAL: 2645 return UIProvider.MessageTextSize.NORMAL; 2646 case Preferences.TEXT_ZOOM_SMALL: 2647 return UIProvider.MessageTextSize.SMALL; 2648 case Preferences.TEXT_ZOOM_TINY: 2649 return UIProvider.MessageTextSize.TINY; 2650 default: 2651 return UIProvider.MessageTextSize.NORMAL; 2652 } 2653 } 2654 2655 /** 2656 * Generate a Uri string for a combined mailbox uri 2657 * @param type the uri command type (e.g. "uimessages") 2658 * @param id the id of the item (e.g. an account, mailbox, or message id) 2659 * @return a Uri string 2660 */ 2661 private static String combinedUriString(String type, String id) { 2662 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 2663 } 2664 2665 public static final long COMBINED_ACCOUNT_ID = 0x10000000; 2666 2667 /** 2668 * Generate an id for a combined mailbox of a given type 2669 * @param type the mailbox type for the combined mailbox 2670 * @return the id, as a String 2671 */ 2672 private static String combinedMailboxId(int type) { 2673 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 2674 } 2675 2676 public static long getVirtualMailboxId(long accountId, int type) { 2677 return (accountId << 32) + type; 2678 } 2679 2680 private static boolean isVirtualMailbox(long mailboxId) { 2681 return mailboxId >= 0x100000000L; 2682 } 2683 2684 private static boolean isCombinedMailbox(long mailboxId) { 2685 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 2686 } 2687 2688 private static long getVirtualMailboxAccountId(long mailboxId) { 2689 return mailboxId >> 32; 2690 } 2691 2692 private static String getVirtualMailboxAccountIdString(long mailboxId) { 2693 return Long.toString(mailboxId >> 32); 2694 } 2695 2696 private static int getVirtualMailboxType(long mailboxId) { 2697 return (int)(mailboxId & 0xF); 2698 } 2699 2700 private void addCombinedAccountRow(MatrixCursor mc) { 2701 final long lastUsedAccountId = 2702 Preferences.getPreferences(getContext()).getLastUsedAccountId(); 2703 final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId); 2704 if (id == Account.NO_ACCOUNT) return; 2705 2706 // Build a map of the requested columns to the appropriate positions 2707 final ImmutableMap.Builder<String, Integer> builder = 2708 new ImmutableMap.Builder<String, Integer>(); 2709 final String[] columnNames = mc.getColumnNames(); 2710 for (int i = 0; i < columnNames.length; i++) { 2711 builder.put(columnNames[i], i); 2712 } 2713 final Map<String, Integer> colPosMap = builder.build(); 2714 2715 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 2716 2717 final Object[] values = new Object[columnNames.length]; 2718 if (colPosMap.containsKey(BaseColumns._ID)) { 2719 values[colPosMap.get(BaseColumns._ID)] = 0; 2720 } 2721 if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) { 2722 values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] = 2723 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 2724 } 2725 if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) { 2726 values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] = 2727 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 2728 } 2729 if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) { 2730 values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString( 2731 R.string.mailbox_list_account_selector_combined_view); 2732 } 2733 if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) { 2734 values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown"; 2735 } 2736 if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) { 2737 values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] = 2738 "'content://" + EmailContent.AUTHORITY + "/uiundo'"; 2739 } 2740 if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) { 2741 values[colPosMap.get(UIProvider.AccountColumns.URI)] = 2742 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 2743 } 2744 if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) { 2745 values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] = 2746 EMAIL_APP_MIME_TYPE; 2747 } 2748 if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2749 values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] = 2750 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 2751 } 2752 if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) { 2753 values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] = 2754 getExternalUriStringEmail2("compose", Long.toString(id)); 2755 } 2756 2757 // TODO: Get these from default account? 2758 Preferences prefs = Preferences.getPreferences(getContext()); 2759 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 2760 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] = 2761 Integer.toString(UIProvider.AutoAdvance.NEWER); 2762 } 2763 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 2764 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] = 2765 Integer.toString(UIProvider.MessageTextSize.NORMAL); 2766 } 2767 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { 2768 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] = 2769 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 2770 } 2771 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 2772 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 2773 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] = 2774 Integer.toString(mailPrefs.getDefaultReplyAll() 2775 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 2776 : UIProvider.DefaultReplyBehavior.REPLY); 2777 } 2778 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 2779 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] = 2780 getConversationListIcon(mailPrefs); 2781 } 2782 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 2783 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] = 2784 prefs.getConfirmDelete() ? 1 : 0; 2785 } 2786 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { 2787 values[colPosMap.get( 2788 UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0; 2789 } 2790 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 2791 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] = 2792 prefs.getConfirmSend() ? 1 : 0; 2793 } 2794 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { 2795 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] = 2796 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 2797 } 2798 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) { 2799 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] = 2800 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 2801 } 2802 2803 mc.addRow(values); 2804 } 2805 2806 private static int getConversationListIcon(MailPrefs mailPrefs) { 2807 return mailPrefs.getShowSenderImages() ? 2808 UIProvider.ConversationListIcon.SENDER_IMAGE : 2809 UIProvider.ConversationListIcon.NONE; 2810 } 2811 2812 private Cursor getVirtualMailboxCursor(long mailboxId) { 2813 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1); 2814 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 2815 getVirtualMailboxType(mailboxId))); 2816 return mc; 2817 } 2818 2819 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 2820 final long id = getVirtualMailboxId(accountId, mailboxType); 2821 final String idString = Long.toString(id); 2822 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 2823 values[UIProvider.FOLDER_ID_COLUMN] = id; 2824 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 2825 values[UIProvider.FOLDER_NAME_COLUMN] = getFolderDisplayName( 2826 getFolderTypeFromMailboxType(mailboxType), ""); 2827 // default empty string since all of these should use resource strings 2828 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 2829 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = UIProvider.FolderCapabilities.IS_VIRTUAL; 2830 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 2831 idString); 2832 2833 // Do any special handling 2834 final String accountIdString = Long.toString(accountId); 2835 switch (mailboxType) { 2836 case Mailbox.TYPE_INBOX: 2837 if (accountId == COMBINED_ACCOUNT_ID) { 2838 // Add the unread count 2839 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 2840 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID 2841 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE 2842 + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", 2843 null); 2844 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 2845 } 2846 // Add the icon 2847 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_inbox; 2848 break; 2849 case Mailbox.TYPE_UNREAD: { 2850 // Add the unread count 2851 final String accountKeyClause; 2852 final String[] whereArgs; 2853 if (accountId == COMBINED_ACCOUNT_ID) { 2854 accountKeyClause = ""; 2855 whereArgs = null; 2856 } else { 2857 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 2858 whereArgs = new String[] { accountIdString }; 2859 } 2860 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 2861 accountKeyClause + MessageColumns.FLAG_READ + "=0 AND " 2862 + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns.ID 2863 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "=" 2864 + Mailbox.TYPE_TRASH + ")", whereArgs); 2865 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 2866 // Add the icon 2867 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_mark_unread; 2868 break; 2869 } case Mailbox.TYPE_STARRED: { 2870 // Add the starred count as the unread count 2871 final String accountKeyClause; 2872 final String[] whereArgs; 2873 if (accountId == COMBINED_ACCOUNT_ID) { 2874 accountKeyClause = ""; 2875 whereArgs = null; 2876 } else { 2877 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 2878 whereArgs = new String[] { accountIdString }; 2879 } 2880 final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI, 2881 accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs); 2882 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = starredCount; 2883 // Add the icon 2884 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_star; 2885 break; 2886 } 2887 } 2888 2889 return values; 2890 } 2891 2892 private Cursor uiAccounts(String[] uiProjection) { 2893 final Context context = getContext(); 2894 final SQLiteDatabase db = getDatabase(context); 2895 final Cursor accountIdCursor = 2896 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 2897 final MatrixCursor mc; 2898 try { 2899 int numAccounts = accountIdCursor.getCount(); 2900 boolean combinedAccount = false; 2901 if (numAccounts > 1) { 2902 combinedAccount = true; 2903 numAccounts++; 2904 } 2905 final Bundle extras = new Bundle(); 2906 // Email always returns the accurate number of accounts 2907 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 2908 mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 2909 final Object[] values = new Object[uiProjection.length]; 2910 while (accountIdCursor.moveToNext()) { 2911 final String id = accountIdCursor.getString(0); 2912 final Cursor accountCursor = 2913 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 2914 try { 2915 if (accountCursor.moveToNext()) { 2916 for (int i = 0; i < uiProjection.length; i++) { 2917 values[i] = accountCursor.getString(i); 2918 } 2919 mc.addRow(values); 2920 } 2921 } finally { 2922 accountCursor.close(); 2923 } 2924 } 2925 if (combinedAccount) { 2926 addCombinedAccountRow(mc); 2927 } 2928 } finally { 2929 accountIdCursor.close(); 2930 } 2931 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER); 2932 2933 return mc; 2934 } 2935 2936 /** 2937 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 2938 * 2939 * @param uiProjection as passed from UnifiedEmail 2940 * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments 2941 * or null if there are no query parameters 2942 * @return the SQLite query to be executed on the EmailProvider database 2943 */ 2944 private static String genQueryAttachments(String[] uiProjection, 2945 List<String> contentTypeQueryParameters) { 2946 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT 2947 ContentValues values = new ContentValues(1); 2948 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 2949 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 2950 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.MESSAGE_KEY + 2951 " =? "); 2952 2953 // Filter for certain content types. 2954 // The filter works by adding LIKE operators for each 2955 // content type you wish to request. Content types 2956 // are filtered by performing a case-insensitive "starts with" 2957 // filter. IE, "image/" would return "image/png" as well as "image/jpeg". 2958 if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) { 2959 final int size = contentTypeQueryParameters.size(); 2960 sb.append("AND ("); 2961 for (int i = 0; i < size; i++) { 2962 final String contentType = contentTypeQueryParameters.get(i); 2963 sb.append(AttachmentColumns.MIME_TYPE + " LIKE '" + contentType + "%'"); 2964 2965 if (i != size - 1) { 2966 sb.append(" OR "); 2967 } 2968 } 2969 sb.append(")"); 2970 } 2971 return sb.toString(); 2972 } 2973 2974 /** 2975 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 2976 * 2977 * @param uiProjection as passed from UnifiedEmail 2978 * @return the SQLite query to be executed on the EmailProvider database 2979 */ 2980 private String genQueryAttachment(String[] uiProjection, String idString) { 2981 Long id = Long.parseLong(idString); 2982 Attachment att = Attachment.restoreAttachmentWithId(getContext(), id); 2983 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS 2984 ContentValues values = new ContentValues(2); 2985 values.put(AttachmentColumns.CONTENT_URI, 2986 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString()); 2987 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 2988 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 2989 sb.append(" FROM " + Attachment.TABLE_NAME + " WHERE " + AttachmentColumns.ID + " =? "); 2990 return sb.toString(); 2991 } 2992 2993 /** 2994 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 2995 * 2996 * @param uiProjection as passed from UnifiedEmail 2997 * @return the SQLite query to be executed on the EmailProvider database 2998 */ 2999 private static String genQuerySubfolders(String[] uiProjection) { 3000 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3001 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 3002 " =? ORDER BY "); 3003 sb.append(MAILBOX_ORDER_BY); 3004 return sb.toString(); 3005 } 3006 3007 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 3008 3009 /** 3010 * Returns a cursor over all the folders for a specific URI which corresponds to a single 3011 * account. 3012 * @param uri 3013 * @param uiProjection 3014 * @return 3015 */ 3016 private Cursor uiFolders(Uri uri, String[] uiProjection) { 3017 Context context = getContext(); 3018 SQLiteDatabase db = getDatabase(context); 3019 String id = uri.getPathSegments().get(1); 3020 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3021 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 3); 3022 Object[] row; 3023 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX); 3024 mc.addRow(row); 3025 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED); 3026 mc.addRow(row); 3027 row = getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD); 3028 mc.addRow(row); 3029 return mc; 3030 } else { 3031 Cursor c = db.rawQuery(genQueryAccountMailboxes(uiProjection), new String[] {id}); 3032 c = getFolderListCursor(db, c, uiProjection); 3033 // Add starred virtual folder to the cursor 3034 // Show number of messages as unread count (for backward compatibility) 3035 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 2); 3036 final long acctId = Long.parseLong(id); 3037 Object[] row = getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED); 3038 mc.addRow(row); 3039 row = getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD); 3040 mc.addRow(row); 3041 3042 mc.setNotificationUri(context.getContentResolver(), uri); 3043 c.setNotificationUri(context.getContentResolver(), uri); 3044 Cursor[] cursors = new Cursor[] {mc, c}; 3045 return new MergeCursor(cursors); 3046 } 3047 } 3048 3049 /** 3050 * Returns an array of the default recent folders for a given URI which is unique for an 3051 * account. Some accounts might not have default recent folders, in which case an empty array 3052 * is returned. 3053 * @param id 3054 * @return 3055 */ 3056 private Uri[] defaultRecentFolders(final String id) { 3057 final SQLiteDatabase db = getDatabase(getContext()); 3058 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3059 // We don't have default recents for the combined view. 3060 return new Uri[0]; 3061 } 3062 // We search for the types we want, and find corresponding IDs. 3063 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 3064 3065 // Sent, Drafts, and Starred are the default recents. 3066 final StringBuilder sb = genSelect(getFolderListMap(), idAndType); 3067 sb.append(" FROM " + Mailbox.TABLE_NAME 3068 + " WHERE " + MailboxColumns.ACCOUNT_KEY + " = " + id 3069 + " AND " 3070 + MailboxColumns.TYPE + " IN (" + Mailbox.TYPE_SENT + 3071 ", " + Mailbox.TYPE_DRAFTS + 3072 ", " + Mailbox.TYPE_STARRED 3073 + ")"); 3074 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 3075 final Cursor c = db.rawQuery(sb.toString(), null); 3076 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 3077 return new Uri[0]; 3078 } 3079 // Read all the IDs of the mailboxes, and turn them into URIs. 3080 final Uri[] recentFolders = new Uri[c.getCount()]; 3081 int i = 0; 3082 do { 3083 final long folderId = c.getLong(0); 3084 recentFolders[i] = uiUri("uifolder", folderId); 3085 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 3086 ++i; 3087 } while (c.moveToNext()); 3088 return recentFolders; 3089 } 3090 3091 /** 3092 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 3093 * any pending notifications for the corresponding mailbox should be canceled). We also handle 3094 * getExtras() to provide a snapshot of the mailbox's status 3095 */ 3096 static class EmailConversationCursor extends CursorWrapper { 3097 private final long mMailboxId; 3098 private final Context mContext; 3099 private final FolderList mFolderList; 3100 private final Bundle mExtras = new Bundle(); 3101 3102 public EmailConversationCursor(final Context context, final Cursor cursor, 3103 final Folder folder, final long mailboxId) { 3104 super(cursor); 3105 mMailboxId = mailboxId; 3106 mContext = context; 3107 mFolderList = FolderList.copyOf(Lists.newArrayList(folder)); 3108 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3109 3110 // We assume that all message lists are complete 3111 // since we don't do any live lists in email. 3112 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3113 UIProvider.CursorStatus.COMPLETE); 3114 if (mailbox != null) { 3115 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3116 mailbox.mUiLastSyncResult); 3117 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount); 3118 } else { 3119 // TODO for virtual mailboxes, we may want to do something besides just fake it 3120 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3121 UIProvider.LastSyncResult.SUCCESS); 3122 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, 3123 cursor != null ? cursor.getCount() : 0); 3124 } 3125 } 3126 3127 @Override 3128 public Bundle getExtras() { 3129 return mExtras; 3130 } 3131 3132 /** 3133 * When showing a folder, if it's been at least this long since the last sync, 3134 * force a folder refresh. 3135 */ 3136 private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS; 3137 3138 @Override 3139 public Bundle respond(Bundle params) { 3140 final Bundle response = new Bundle(); 3141 3142 final String setVisibilityKey = 3143 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3144 if (params.containsKey(setVisibilityKey)) { 3145 final boolean visible = params.getBoolean(setVisibilityKey); 3146 if (visible) { 3147 if (params.containsKey( 3148 UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { 3149 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 3150 if (mailbox != null) { 3151 final ContentResolver resolver = mContext.getContentResolver(); 3152 // Mark all messages as seen 3153 // TODO: should this happen even if the mailbox couldn't be restored? 3154 final ContentValues contentValues = new ContentValues(1); 3155 contentValues.put(MessageColumns.FLAG_SEEN, true); 3156 final Uri uri = EmailContent.Message.CONTENT_URI; 3157 resolver.update(uri, contentValues, MessageColumns.MAILBOX_KEY + " = ?", 3158 new String[] {String.valueOf(mailbox.mId)}); 3159 // For non-push mailboxes, if it's stale (i.e. last sync was a while 3160 // ago), force a sync. 3161 // TODO: Fix the check for whether we're non-push? Right now it checks 3162 // whether we are participating in account sync rules. 3163 if (mailbox.mSyncInterval == 0) { 3164 final long timeSinceLastSync = 3165 System.currentTimeMillis() - mailbox.mSyncTime; 3166 if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) { 3167 final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI + 3168 "/" + QUERY_UIREFRESH + "/" + mailbox.mId); 3169 resolver.query(refreshUri, null, null, null, null); 3170 } 3171 } 3172 } 3173 } 3174 } 3175 } 3176 // Return success 3177 response.putString(setVisibilityKey, 3178 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3179 3180 final String rawFoldersKey = 3181 UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS; 3182 if (params.containsKey(rawFoldersKey)) { 3183 response.putParcelable(rawFoldersKey, mFolderList); 3184 } 3185 3186 return response; 3187 } 3188 } 3189 3190 /** 3191 * Convenience method to create a {@link Folder} 3192 * @param context to get a {@ContentResolver} 3193 * @param mailboxId id of the {@link Mailbox} that we want 3194 * @return the {@link Folder} or null 3195 */ 3196 public static Folder getFolder(Context context, long mailboxId) { 3197 final ContentResolver resolver = context.getContentResolver(); 3198 final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId), 3199 UIProvider.FOLDERS_PROJECTION, null, null, null); 3200 3201 if (fc == null) { 3202 LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId); 3203 return null; 3204 } 3205 3206 Folder uiFolder = null; 3207 try { 3208 if (fc.moveToFirst()) { 3209 uiFolder = new Folder(fc); 3210 } 3211 } finally { 3212 fc.close(); 3213 } 3214 return uiFolder; 3215 } 3216 3217 static class AttachmentsCursor extends CursorWrapper { 3218 private final int mContentUriIndex; 3219 private final int mUriIndex; 3220 private final Context mContext; 3221 3222 public AttachmentsCursor(Context context, Cursor cursor) { 3223 super(cursor); 3224 mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI); 3225 mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI); 3226 mContext = context; 3227 } 3228 3229 @Override 3230 public String getString(int column) { 3231 if (column == mContentUriIndex) { 3232 final Uri uri = Uri.parse(getString(mUriIndex)); 3233 final long id = Long.parseLong(uri.getLastPathSegment()); 3234 final Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 3235 if (att == null) return ""; 3236 3237 final String contentUri; 3238 // Until the package installer can handle opening apks from a content:// uri, for 3239 // any apk that was successfully saved in external storage, return the 3240 // content uri from the attachment 3241 if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL && 3242 att.mUiState == UIProvider.AttachmentState.SAVED && 3243 TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) { 3244 contentUri = att.getContentUri(); 3245 } else { 3246 contentUri = 3247 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString(); 3248 } 3249 return contentUri; 3250 } else { 3251 return super.getString(column); 3252 } 3253 } 3254 } 3255 3256 /** 3257 * For debugging purposes; shouldn't be used in production code 3258 */ 3259 static class CloseDetectingCursor extends CursorWrapper { 3260 3261 public CloseDetectingCursor(Cursor cursor) { 3262 super(cursor); 3263 } 3264 3265 @Override 3266 public void close() { 3267 super.close(); 3268 LogUtils.d(TAG, "Closing cursor", new Error()); 3269 } 3270 } 3271 3272 /** 3273 * We need to do individual queries for the mailboxes in order to get correct 3274 * folder capabilities. 3275 */ 3276 private Cursor getFolderListCursor(SQLiteDatabase db, Cursor c, String[] uiProjection) { 3277 final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); 3278 final String[] args = new String[1]; 3279 final int projectionLength = uiProjection.length; 3280 final List<String> projectionList = Arrays.asList(uiProjection); 3281 final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); 3282 final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); 3283 3284 try { 3285 // Loop through mailboxes, building matrix cursor 3286 while (c.moveToNext()) { 3287 final String id = c.getString(0); 3288 args[0] = id; 3289 final Cursor mailboxCursor = db.rawQuery(genQueryMailbox(uiProjection, id), args); 3290 if (mailboxCursor.moveToNext()) { 3291 getUiFolderCursorRowFromMailboxCursorRow( 3292 mc, projectionLength, mailboxCursor, nameColumn, typeColumn); 3293 } 3294 } 3295 } finally { 3296 c.close(); 3297 } 3298 return mc; 3299 } 3300 3301 /** 3302 * Converts a mailbox in a row of the mailboxCursor into a row 3303 * in the supplied {@link MatrixCursor} in the format required for {@link Folder}. 3304 * As a convenience, the modified {@link MatrixCursor} is also returned. 3305 * @param mc the {@link MatrixCursor} into which the mailbox data will be converted 3306 * @param projectionLength the length of the projection for this Cursor 3307 * @param mailboxCursor the cursor supplying the mailbox data 3308 * @param nameColumn column in the cursor containing the folder name value 3309 * @param typeColumn column in the cursor containing the folder type value 3310 * @return the {@link MatrixCursor} containing the transformed data. 3311 */ 3312 private Cursor getUiFolderCursorRowFromMailboxCursorRow( 3313 MatrixCursor mc, int projectionLength, Cursor mailboxCursor, 3314 int nameColumn, int typeColumn) { 3315 final MatrixCursor.RowBuilder builder = mc.newRow(); 3316 for (int i = 0; i < projectionLength; i++) { 3317 // If we are at the name column, get the type 3318 // and use it to use a properly translated string 3319 // from resources instead of the display name. 3320 // This ignores display names for system mailboxes. 3321 if (nameColumn == i) { 3322 // We implicitly assume that if name is requested, 3323 // type has also been requested. If not, this will 3324 // error in unknown ways. 3325 final int type = mailboxCursor.getInt(typeColumn); 3326 builder.add(getFolderDisplayName(type, mailboxCursor.getString(i))); 3327 } else { 3328 builder.add(mailboxCursor.getString(i)); 3329 } 3330 } 3331 return mc; 3332 } 3333 3334 /** 3335 * Returns a {@link String} from Resources corresponding 3336 * to the {@link UIProvider.FolderType} requested. 3337 * @param folderType {@link UIProvider.FolderType} value for the folder 3338 * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType} 3339 * provided is not a system folder. 3340 * @return a {@link String} to use as the display name for the folder 3341 */ 3342 private String getFolderDisplayName(int folderType, String defaultName) { 3343 int resId = -1; 3344 switch (folderType) { 3345 case UIProvider.FolderType.INBOX: 3346 resId = R.string.mailbox_name_display_inbox; 3347 break; 3348 case UIProvider.FolderType.OUTBOX: 3349 resId = R.string.mailbox_name_display_outbox; 3350 break; 3351 case UIProvider.FolderType.DRAFT: 3352 resId = R.string.mailbox_name_display_drafts; 3353 break; 3354 case UIProvider.FolderType.TRASH: 3355 resId = R.string.mailbox_name_display_trash; 3356 break; 3357 case UIProvider.FolderType.SENT: 3358 resId = R.string.mailbox_name_display_sent; 3359 break; 3360 case UIProvider.FolderType.SPAM: 3361 resId = R.string.mailbox_name_display_junk; 3362 break; 3363 case UIProvider.FolderType.STARRED: 3364 resId = R.string.mailbox_name_display_starred; 3365 break; 3366 case UIProvider.FolderType.UNREAD: 3367 resId = R.string.mailbox_name_display_unread; 3368 break; 3369 default: 3370 return defaultName; 3371 } 3372 return getContext().getString(resId); 3373 } 3374 3375 /** 3376 * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType} 3377 * equivalent. 3378 * @param mailboxType a {@link Mailbox} type 3379 * @return a {@link UIProvider.FolderType} value 3380 */ 3381 private static int getFolderTypeFromMailboxType(int mailboxType) { 3382 switch (mailboxType) { 3383 case Mailbox.TYPE_INBOX: 3384 return UIProvider.FolderType.INBOX; 3385 case Mailbox.TYPE_OUTBOX: 3386 return UIProvider.FolderType.OUTBOX; 3387 case Mailbox.TYPE_DRAFTS: 3388 return UIProvider.FolderType.DRAFT; 3389 case Mailbox.TYPE_TRASH: 3390 return UIProvider.FolderType.TRASH; 3391 case Mailbox.TYPE_SENT: 3392 return UIProvider.FolderType.SENT; 3393 case Mailbox.TYPE_JUNK: 3394 return UIProvider.FolderType.SPAM; 3395 case Mailbox.TYPE_STARRED: 3396 return UIProvider.FolderType.STARRED; 3397 case Mailbox.TYPE_UNREAD: 3398 return UIProvider.FolderType.UNREAD; 3399 default: 3400 return UIProvider.FolderType.DEFAULT; 3401 } 3402 } 3403 3404 /** 3405 * Handle UnifiedEmail queries here (dispatched from query()) 3406 * 3407 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 3408 * @param uri the original uri passed in from UnifiedEmail 3409 * @param uiProjection the projection passed in from UnifiedEmail 3410 * @param unseenOnly <code>true</code> to only return unseen messages (where supported) 3411 * @return the result Cursor 3412 */ 3413 private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) { 3414 Context context = getContext(); 3415 ContentResolver resolver = context.getContentResolver(); 3416 SQLiteDatabase db = getDatabase(context); 3417 // Should we ever return null, or throw an exception?? 3418 Cursor c = null; 3419 String id = uri.getPathSegments().get(1); 3420 Uri notifyUri = null; 3421 switch(match) { 3422 case UI_ALL_FOLDERS: 3423 c = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), new String[] {id}); 3424 c = getFolderListCursor(db, c, uiProjection); 3425 break; 3426 case UI_RECENT_FOLDERS: 3427 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 3428 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3429 break; 3430 case UI_SUBFOLDERS: 3431 c = db.rawQuery(genQuerySubfolders(uiProjection), new String[] {id}); 3432 c = getFolderListCursor(db, c, uiProjection); 3433 break; 3434 case UI_MESSAGES: 3435 long mailboxId = Long.parseLong(id); 3436 final Folder folder = getFolder(context, mailboxId); 3437 if (folder == null) { 3438 // This mailboxId is bogus. 3439 return null; 3440 } 3441 if (isVirtualMailbox(mailboxId)) { 3442 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly); 3443 } else { 3444 c = db.rawQuery( 3445 genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id}); 3446 } 3447 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 3448 c = new EmailConversationCursor(context, c, folder, mailboxId); 3449 break; 3450 case UI_MESSAGE: 3451 MessageQuery qq = genQueryViewMessage(uiProjection, id); 3452 String sql = qq.query; 3453 String attJson = qq.attachmentJson; 3454 // With attachments, we have another argument to bind 3455 if (attJson != null) { 3456 c = db.rawQuery(sql, new String[] {attJson, id}); 3457 } else { 3458 c = db.rawQuery(sql, new String[] {id}); 3459 } 3460 break; 3461 case UI_ATTACHMENTS: 3462 final List<String> contentTypeQueryParameters = 3463 uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE); 3464 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters), 3465 new String[] {id}); 3466 c = new AttachmentsCursor(context, c); 3467 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 3468 break; 3469 case UI_ATTACHMENT: 3470 c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id}); 3471 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 3472 break; 3473 case UI_FOLDER: 3474 mailboxId = Long.parseLong(id); 3475 if (isVirtualMailbox(mailboxId)) { 3476 c = getVirtualMailboxCursor(mailboxId); 3477 } else { 3478 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[]{id}); 3479 final List<String> projectionList = Arrays.asList(uiProjection); 3480 final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); 3481 final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); 3482 if (c.moveToFirst()) { 3483 c = getUiFolderCursorRowFromMailboxCursorRow( 3484 new MatrixCursorWithCachedColumns(uiProjection), 3485 uiProjection.length, c, nameColumn, typeColumn); 3486 } 3487 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 3488 } 3489 break; 3490 case UI_ACCOUNT: 3491 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3492 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1); 3493 addCombinedAccountRow(mc); 3494 c = mc; 3495 } else { 3496 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3497 } 3498 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 3499 break; 3500 case UI_CONVERSATION: 3501 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 3502 break; 3503 } 3504 if (notifyUri != null) { 3505 c.setNotificationUri(resolver, notifyUri); 3506 } 3507 return c; 3508 } 3509 3510 /** 3511 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 3512 * a few of the fields 3513 * @param uiAtt the UIProvider attachment to convert 3514 * @param cachedFile the path to the cached file to 3515 * @return the EmailProvider attachment 3516 */ 3517 // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be 3518 // removed 3519 private static Attachment convertUiAttachmentToAttachment( 3520 com.android.mail.providers.Attachment uiAtt, String cachedFile) { 3521 final Attachment att = new Attachment(); 3522 att.setContentUri(uiAtt.contentUri.toString()); 3523 3524 if (!TextUtils.isEmpty(cachedFile)) { 3525 // Generate the content provider uri for this cached file 3526 final Uri.Builder cachedFileBuilder = Uri.parse( 3527 "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon(); 3528 cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile); 3529 att.setCachedFileUri(cachedFileBuilder.build().toString()); 3530 } 3531 3532 att.mFileName = uiAtt.getName(); 3533 att.mMimeType = uiAtt.getContentType(); 3534 att.mSize = uiAtt.size; 3535 return att; 3536 } 3537 3538 /** 3539 * Create a mailbox given the account and mailboxType. 3540 */ 3541 private Mailbox createMailbox(long accountId, int mailboxType) { 3542 Context context = getContext(); 3543 Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType); 3544 // Make sure drafts and save will show up in recents... 3545 // If these already exist (from old Email app), they will have touch times 3546 switch (mailboxType) { 3547 case Mailbox.TYPE_DRAFTS: 3548 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 3549 break; 3550 case Mailbox.TYPE_SENT: 3551 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 3552 break; 3553 } 3554 box.save(context); 3555 return box; 3556 } 3557 3558 /** 3559 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 3560 * @param accountId the account id to use 3561 * @param mailboxType the type of mailbox we're trying to find 3562 * @return the mailbox of the given type for the account in the uri, or null if not found 3563 */ 3564 private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) { 3565 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 3566 if (mailbox == null) { 3567 mailbox = createMailbox(accountId, mailboxType); 3568 } 3569 return mailbox; 3570 } 3571 3572 /** 3573 * Given a mailbox and the content values for a message, create/save the message in the mailbox 3574 * @param mailbox the mailbox to use 3575 * @param extras the bundle containing the message fields 3576 * @return the uri of the newly created message 3577 * TODO(yph): The following fields are available in extras but unused, verify whether they 3578 * should be respected: 3579 * - UIProvider.MessageColumns.SNIPPET 3580 * - UIProvider.MessageColumns.REPLY_TO 3581 * - UIProvider.MessageColumns.FROM 3582 * - UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS 3583 */ 3584 private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) { 3585 final Context context = getContext(); 3586 // Fill in the message 3587 final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 3588 if (account == null) return null; 3589 msg.mFrom = account.mEmailAddress; 3590 msg.mTimeStamp = System.currentTimeMillis(); 3591 msg.mTo = extras.getString(UIProvider.MessageColumns.TO); 3592 msg.mCc = extras.getString(UIProvider.MessageColumns.CC); 3593 msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC); 3594 msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT); 3595 msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT); 3596 msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML); 3597 msg.mMailboxKey = mailbox.mId; 3598 msg.mAccountKey = mailbox.mAccountKey; 3599 msg.mDisplayName = msg.mTo; 3600 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 3601 msg.mFlagRead = true; 3602 msg.mFlagSeen = true; 3603 final Integer quoteStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 3604 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 3605 int flags = 0; 3606 final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE); 3607 switch(draftType) { 3608 case DraftType.FORWARD: 3609 flags |= Message.FLAG_TYPE_FORWARD; 3610 break; 3611 case DraftType.REPLY_ALL: 3612 flags |= Message.FLAG_TYPE_REPLY_ALL; 3613 //$FALL-THROUGH$ 3614 case DraftType.REPLY: 3615 flags |= Message.FLAG_TYPE_REPLY; 3616 break; 3617 case DraftType.COMPOSE: 3618 flags |= Message.FLAG_TYPE_ORIGINAL; 3619 break; 3620 } 3621 int draftInfo = 0; 3622 if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 3623 draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 3624 if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 3625 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 3626 } 3627 } 3628 if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) { 3629 flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT; 3630 } 3631 msg.mDraftInfo = draftInfo; 3632 msg.mFlags = flags; 3633 3634 final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID); 3635 if (ref != null && msg.mQuotedTextStartPos >= 0) { 3636 String refId = Uri.parse(ref).getLastPathSegment(); 3637 try { 3638 long sourceKey = Long.parseLong(refId); 3639 msg.mSourceKey = sourceKey; 3640 } catch (NumberFormatException e) { 3641 // This will be zero; the default 3642 } 3643 } 3644 3645 // Get attachments from the ContentValues 3646 final List<com.android.mail.providers.Attachment> uiAtts = 3647 com.android.mail.providers.Attachment.fromJSONArray( 3648 extras.getString(UIProvider.MessageColumns.ATTACHMENTS)); 3649 final ArrayList<Attachment> atts = new ArrayList<Attachment>(); 3650 boolean hasUnloadedAttachments = false; 3651 Bundle attachmentFds = 3652 extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP); 3653 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 3654 final Uri attUri = uiAtt.uri; 3655 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 3656 // If it's one of ours, retrieve the attachment and add it to the list 3657 final long attId = Long.parseLong(attUri.getLastPathSegment()); 3658 final Attachment att = Attachment.restoreAttachmentWithId(context, attId); 3659 if (att != null) { 3660 // We must clone the attachment into a new one for this message; easiest to 3661 // use a parcel here 3662 final Parcel p = Parcel.obtain(); 3663 att.writeToParcel(p, 0); 3664 p.setDataPosition(0); 3665 final Attachment attClone = new Attachment(p); 3666 p.recycle(); 3667 // Clear the messageKey (this is going to be a new attachment) 3668 attClone.mMessageKey = 0; 3669 // If we're sending this, it's not loaded, and we're not smart forwarding 3670 // add the download flag, so that ADS will start up 3671 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null && 3672 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 3673 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 3674 hasUnloadedAttachments = true; 3675 } 3676 atts.add(attClone); 3677 } 3678 } else { 3679 // Cache the attachment. This will allow us to send it, if the permissions are 3680 // revoked 3681 final String cachedFileUri = 3682 AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds); 3683 3684 // Convert external attachment to one of ours and add to the list 3685 atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri)); 3686 } 3687 } 3688 if (!atts.isEmpty()) { 3689 msg.mAttachments = atts; 3690 msg.mFlagAttachment = true; 3691 if (hasUnloadedAttachments) { 3692 Utility.showToast(context, R.string.message_view_attachment_background_load); 3693 } 3694 } 3695 // Save it or update it... 3696 if (!msg.isSaved()) { 3697 msg.save(context); 3698 } else { 3699 // This is tricky due to how messages/attachments are saved; rather than putz with 3700 // what's changed, we'll delete/re-add them 3701 final ArrayList<ContentProviderOperation> ops = 3702 new ArrayList<ContentProviderOperation>(); 3703 // Delete all existing attachments 3704 ops.add(ContentProviderOperation.newDelete( 3705 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 3706 .build()); 3707 // Delete the body 3708 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 3709 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 3710 .build()); 3711 // Add the ops for the message, atts, and body 3712 msg.addSaveOps(ops); 3713 // Do it! 3714 try { 3715 applyBatch(ops); 3716 } catch (OperationApplicationException e) { 3717 } 3718 } 3719 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 3720 startSync(mailbox, 0); 3721 final long originalMsgId = msg.mSourceKey; 3722 if (originalMsgId != 0) { 3723 final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 3724 // If the original message exists, set its forwarded/replied to flags 3725 if (originalMsg != null) { 3726 final ContentValues cv = new ContentValues(); 3727 flags = originalMsg.mFlags; 3728 switch(draftType) { 3729 case DraftType.FORWARD: 3730 flags |= Message.FLAG_FORWARDED; 3731 break; 3732 case DraftType.REPLY_ALL: 3733 case DraftType.REPLY: 3734 flags |= Message.FLAG_REPLIED_TO; 3735 break; 3736 } 3737 cv.put(Message.FLAGS, flags); 3738 context.getContentResolver().update(ContentUris.withAppendedId( 3739 Message.CONTENT_URI, originalMsgId), cv, null, null); 3740 } 3741 } 3742 } 3743 return uiUri("uimessage", msg.mId); 3744 } 3745 3746 private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) { 3747 final Mailbox mailbox = 3748 getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS); 3749 if (mailbox == null) return null; 3750 final Message msg; 3751 if (extras.containsKey(BaseColumns._ID)) { 3752 final long messageId = extras.getLong(BaseColumns._ID); 3753 msg = Message.restoreMessageWithId(getContext(), messageId); 3754 } else { 3755 msg = new Message(); 3756 } 3757 return uiSaveMessage(msg, mailbox, extras); 3758 } 3759 3760 private Uri uiSendDraftMessage(final long accountId, final Bundle extras) { 3761 final Context context = getContext(); 3762 final Message msg; 3763 if (extras.containsKey(BaseColumns._ID)) { 3764 final long messageId = extras.getLong(BaseColumns._ID); 3765 msg = Message.restoreMessageWithId(getContext(), messageId); 3766 } else { 3767 msg = new Message(); 3768 } 3769 3770 if (msg == null) return null; 3771 final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX); 3772 if (mailbox == null) return null; 3773 // Make sure the sent mailbox exists, since it will be necessary soon. 3774 // TODO(yph): move system mailbox creation to somewhere sane. 3775 final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT); 3776 if (sentMailbox == null) return null; 3777 final Uri messageUri = uiSaveMessage(msg, mailbox, extras); 3778 // Kick observers 3779 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 3780 return messageUri; 3781 } 3782 3783 private static void putIntegerLongOrBoolean(ContentValues values, String columnName, 3784 Object value) { 3785 if (value instanceof Integer) { 3786 Integer intValue = (Integer)value; 3787 values.put(columnName, intValue); 3788 } else if (value instanceof Boolean) { 3789 Boolean boolValue = (Boolean)value; 3790 values.put(columnName, boolValue ? 1 : 0); 3791 } else if (value instanceof Long) { 3792 Long longValue = (Long)value; 3793 values.put(columnName, longValue); 3794 } 3795 } 3796 3797 /** 3798 * Update the timestamps for the folders specified and notifies on the recent folder URI. 3799 * @param folders 3800 * @return number of folders updated 3801 */ 3802 private static int updateTimestamp(final Context context, String id, Uri[] folders){ 3803 int updated = 0; 3804 final long now = System.currentTimeMillis(); 3805 final ContentResolver resolver = context.getContentResolver(); 3806 final ContentValues touchValues = new ContentValues(); 3807 for (int i=0, size=folders.length; i < size; ++i) { 3808 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 3809 LogUtils.d(TAG, "updateStamp: %s updated", folders[i]); 3810 updated += resolver.update(folders[i], touchValues, null, null); 3811 } 3812 final Uri toNotify = 3813 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 3814 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 3815 resolver.notifyChange(toNotify, null); 3816 return updated; 3817 } 3818 3819 /** 3820 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 3821 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 3822 * @param uri 3823 * @param values 3824 * @return nonzero value always. 3825 */ 3826 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 3827 final int numFolders = values.size(); 3828 final String id = uri.getPathSegments().get(1); 3829 final Uri[] folders = new Uri[numFolders]; 3830 final Context context = getContext(); 3831 int i = 0; 3832 for (final String uriString : values.keySet()) { 3833 folders[i] = Uri.parse(uriString); 3834 } 3835 return updateTimestamp(context, id, folders); 3836 } 3837 3838 /** 3839 * Populates the recent folders according to the design. 3840 * @param uri 3841 * @return the number of recent folders were populated. 3842 */ 3843 private int uiPopulateRecentFolders(Uri uri) { 3844 final Context context = getContext(); 3845 final String id = uri.getLastPathSegment(); 3846 final Uri[] recentFolders = defaultRecentFolders(id); 3847 final int numFolders = recentFolders.length; 3848 if (numFolders <= 0) { 3849 return 0; 3850 } 3851 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 3852 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 3853 return rowsUpdated; 3854 } 3855 3856 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 3857 int result = 0; 3858 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 3859 if (stateValue != null) { 3860 // This is a command from UIProvider 3861 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 3862 Context context = getContext(); 3863 Attachment attachment = 3864 Attachment.restoreAttachmentWithId(context, attachmentId); 3865 if (attachment == null) { 3866 // Went away; ah, well... 3867 return result; 3868 } 3869 int state = stateValue.intValue(); 3870 ContentValues values = new ContentValues(); 3871 if (state == UIProvider.AttachmentState.NOT_SAVED 3872 || state == UIProvider.AttachmentState.REDOWNLOADING) { 3873 // Set state, try to cancel request 3874 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED); 3875 values.put(AttachmentColumns.FLAGS, 3876 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 3877 attachment.update(context, values); 3878 result = 1; 3879 } 3880 if (state == UIProvider.AttachmentState.DOWNLOADING 3881 || state == UIProvider.AttachmentState.REDOWNLOADING) { 3882 // Set state and destination; request download 3883 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING); 3884 Integer destinationValue = 3885 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 3886 values.put(AttachmentColumns.UI_DESTINATION, 3887 destinationValue == null ? 0 : destinationValue); 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.SAVED) { 3894 // If this is an inline attachment, notify message has changed 3895 if (!TextUtils.isEmpty(attachment.mContentId)) { 3896 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 3897 } 3898 result = 1; 3899 } 3900 } 3901 return result; 3902 } 3903 3904 private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) { 3905 // We need to mark seen separately 3906 if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) { 3907 final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN); 3908 3909 if (seenValue == 1) { 3910 final String mailboxId = uri.getLastPathSegment(); 3911 final int rows = markAllSeen(context, mailboxId); 3912 3913 if (uiValues.size() == 1) { 3914 // Nothing else to do, so return this value 3915 return rows; 3916 } 3917 } 3918 } 3919 3920 final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 3921 if (ourUri == null) return 0; 3922 ContentValues ourValues = new ContentValues(); 3923 // This should only be called via update to "recent folders" 3924 for (String columnName: uiValues.keySet()) { 3925 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 3926 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 3927 } 3928 } 3929 return update(ourUri, ourValues, null, null); 3930 } 3931 3932 private int markAllSeen(final Context context, final String mailboxId) { 3933 final SQLiteDatabase db = getDatabase(context); 3934 final String table = Message.TABLE_NAME; 3935 final ContentValues values = new ContentValues(1); 3936 values.put(MessageColumns.FLAG_SEEN, 1); 3937 final String whereClause = MessageColumns.MAILBOX_KEY + " = ?"; 3938 final String[] whereArgs = new String[] {mailboxId}; 3939 3940 return db.update(table, values, whereClause, whereArgs); 3941 } 3942 3943 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 3944 final ContentValues ourValues = new ContentValues(); 3945 for (String columnName : values.keySet()) { 3946 final Object val = values.get(columnName); 3947 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 3948 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 3949 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 3950 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 3951 } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) { 3952 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val); 3953 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 3954 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 3955 } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) { 3956 // Skip this column, as the folders will also be specified the RAW_FOLDERS column 3957 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 3958 // Convert from folder list uri to mailbox key 3959 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName)); 3960 if (flist.folders.size() != 1) { 3961 LogUtils.e(TAG, 3962 "Incorrect number of folders for this message: Message is %s", 3963 message.mId); 3964 } else { 3965 final Folder f = flist.folders.get(0); 3966 final Uri uri = f.folderUri.fullUri; 3967 final Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 3968 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 3969 } 3970 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 3971 Address[] fromList = Address.unpack(message.mFrom); 3972 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 3973 for (Address sender : fromList) { 3974 final String email = sender.getAddress(); 3975 mailPrefs.setDisplayImagesFromSender(email, null); 3976 } 3977 } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) || 3978 columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) { 3979 // Ignore for now 3980 } else { 3981 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 3982 } 3983 } 3984 return ourValues; 3985 } 3986 3987 private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 3988 final String idString = uri.getLastPathSegment(); 3989 try { 3990 final long id = Long.parseLong(idString); 3991 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 3992 if (asProvider) { 3993 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 3994 } 3995 return ourUri; 3996 } catch (NumberFormatException e) { 3997 return null; 3998 } 3999 } 4000 4001 private Message getMessageFromLastSegment(Uri uri) { 4002 long messageId = Long.parseLong(uri.getLastPathSegment()); 4003 return Message.restoreMessageWithId(getContext(), messageId); 4004 } 4005 4006 /** 4007 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 4008 * clear out the undo list and start over 4009 * @param uri the uri we're working on 4010 * @param op the ContentProviderOperation to perform upon undo 4011 */ 4012 private void addToSequence(Uri uri, ContentProviderOperation op) { 4013 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 4014 if (sequenceString != null) { 4015 int sequence = Integer.parseInt(sequenceString); 4016 if (sequence > mLastSequence) { 4017 // Reset sequence 4018 mLastSequenceOps.clear(); 4019 mLastSequence = sequence; 4020 } 4021 // TODO: Need something to indicate a change isn't ready (undoable) 4022 mLastSequenceOps.add(op); 4023 } 4024 } 4025 4026 // TODO: This should depend on flags on the mailbox... 4027 private static boolean uploadsToServer(Context context, Mailbox m) { 4028 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 4029 m.mType == Mailbox.TYPE_SEARCH) { 4030 return false; 4031 } 4032 String protocol = Account.getProtocol(context, m.mAccountKey); 4033 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 4034 return (info != null && info.syncChanges); 4035 } 4036 4037 private int uiUpdateMessage(Uri uri, ContentValues values) { 4038 return uiUpdateMessage(uri, values, false); 4039 } 4040 4041 private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) { 4042 Context context = getContext(); 4043 Message msg = getMessageFromLastSegment(uri); 4044 if (msg == null) return 0; 4045 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4046 if (mailbox == null) return 0; 4047 Uri ourBaseUri = 4048 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI : 4049 Message.CONTENT_URI; 4050 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 4051 if (ourUri == null) return 0; 4052 4053 // Special case - meeting response 4054 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 4055 final EmailServiceProxy service = 4056 EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey); 4057 try { 4058 service.sendMeetingResponse(msg.mId, 4059 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 4060 // Delete the message immediately 4061 uiDeleteMessage(uri); 4062 Utility.showToast(context, R.string.confirm_response); 4063 // Notify box has changed so the deletion is reflected in the UI 4064 notifyUIConversationMailbox(mailbox.mId); 4065 } catch (RemoteException e) { 4066 } 4067 return 1; 4068 } 4069 4070 ContentValues undoValues = new ContentValues(); 4071 ContentValues ourValues = convertUiMessageValues(msg, values); 4072 for (String columnName: ourValues.keySet()) { 4073 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4074 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 4075 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 4076 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 4077 } else if (columnName.equals(MessageColumns.FLAG_SEEN)) { 4078 undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen); 4079 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 4080 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 4081 } 4082 } 4083 if (undoValues.size() == 0) { 4084 return -1; 4085 } 4086 final Boolean suppressUndo = 4087 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO); 4088 if (suppressUndo == null || !suppressUndo.booleanValue()) { 4089 final ContentProviderOperation op = 4090 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 4091 uri, ourBaseUri, false)) 4092 .withValues(undoValues) 4093 .build(); 4094 addToSequence(uri, op); 4095 } 4096 return update(ourUri, ourValues, null, null); 4097 } 4098 4099 public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; 4100 public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; 4101 public static final String PICKER_MESSAGE_ID = "picker_message_id"; 4102 public static final String PICKER_HEADER_ID = "picker_header_id"; 4103 4104 private int uiDeleteMessage(Uri uri) { 4105 final Context context = getContext(); 4106 Message msg = getMessageFromLastSegment(uri); 4107 if (msg == null) return 0; 4108 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4109 if (mailbox == null) return 0; 4110 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 4111 // We actually delete these, including attachments 4112 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 4113 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4114 return context.getContentResolver().delete( 4115 ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); 4116 } 4117 Mailbox trashMailbox = 4118 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 4119 if (trashMailbox == null) { 4120 return 0; 4121 } 4122 ContentValues values = new ContentValues(); 4123 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 4124 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4125 return uiUpdateMessage(uri, values, true); 4126 } 4127 4128 private int pickFolder(Uri uri, int type, int headerId) { 4129 Context context = getContext(); 4130 Long acctId = Long.parseLong(uri.getLastPathSegment()); 4131 // For push imap, for example, we want the user to select the trash mailbox 4132 Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, 4133 null, null, null); 4134 try { 4135 if (ac.moveToFirst()) { 4136 final com.android.mail.providers.Account uiAccount = 4137 new com.android.mail.providers.Account(ac); 4138 Intent intent = new Intent(context, FolderPickerActivity.class); 4139 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); 4140 intent.putExtra(PICKER_MAILBOX_TYPE, type); 4141 intent.putExtra(PICKER_HEADER_ID, headerId); 4142 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 4143 context.startActivity(intent); 4144 return 1; 4145 } 4146 return 0; 4147 } finally { 4148 ac.close(); 4149 } 4150 } 4151 4152 private int pickTrashFolder(Uri uri) { 4153 return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title); 4154 } 4155 4156 private int pickSentFolder(Uri uri) { 4157 return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title); 4158 } 4159 4160 private Cursor uiUndo(String[] projection) { 4161 // First see if we have any operations saved 4162 // TODO: Make sure seq matches 4163 if (!mLastSequenceOps.isEmpty()) { 4164 try { 4165 // TODO Always use this projection? Or what's passed in? 4166 // Not sure if UI wants it, but I'm making a cursor of convo uri's 4167 MatrixCursor c = new MatrixCursorWithCachedColumns( 4168 new String[] {UIProvider.ConversationColumns.URI}, 4169 mLastSequenceOps.size()); 4170 for (ContentProviderOperation op: mLastSequenceOps) { 4171 c.addRow(new String[] {op.getUri().toString()}); 4172 } 4173 // Just apply the batch and we're done! 4174 applyBatch(mLastSequenceOps); 4175 // But clear the operations 4176 mLastSequenceOps.clear(); 4177 return c; 4178 } catch (OperationApplicationException e) { 4179 } 4180 } 4181 return new MatrixCursorWithCachedColumns(projection, 0); 4182 } 4183 4184 private void notifyUIConversation(Uri uri) { 4185 String id = uri.getLastPathSegment(); 4186 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 4187 if (msg != null) { 4188 notifyUIConversationMailbox(msg.mMailboxKey); 4189 } 4190 } 4191 4192 /** 4193 * Notify about the Mailbox id passed in 4194 * @param id the Mailbox id to be notified 4195 */ 4196 private void notifyUIConversationMailbox(long id) { 4197 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 4198 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 4199 if (mailbox == null) { 4200 LogUtils.w(TAG, "No mailbox for notification: " + id); 4201 return; 4202 } 4203 // Notify combined inbox... 4204 if (mailbox.mType == Mailbox.TYPE_INBOX) { 4205 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 4206 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 4207 } 4208 notifyWidgets(id); 4209 } 4210 4211 /** 4212 * Notify about the Account id passed in 4213 * @param id the Account id to be notified 4214 */ 4215 private void notifyUIAccount(long id) { 4216 // Notify on the specific account 4217 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id)); 4218 4219 // Notify on the all accounts list 4220 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 4221 } 4222 4223 /** 4224 * Notify about a folder update. Because folder changes can affect the conversation cursor's 4225 * extras, the conversation must also be notified here. 4226 * @param folderId the folder id to be notified 4227 * @param accountId the account id to be notified (for folder list notification); if null, then 4228 * lookup the accountId from the folder. 4229 */ 4230 private void notifyUIFolder(String folderId, String accountId) { 4231 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); 4232 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId); 4233 if (accountId == null) { 4234 try { 4235 final Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), 4236 Long.parseLong(folderId)); 4237 if (mailbox != null) { 4238 accountId = Long.toString(mailbox.mAccountKey); 4239 } 4240 } catch (NumberFormatException e) { 4241 // Bad folderId, so we can't lookup account. 4242 } 4243 } 4244 if (accountId != null) { 4245 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 4246 } 4247 } 4248 4249 private void notifyUIFolder(long folderId, long accountId) { 4250 notifyUIFolder(Long.toString(folderId), Long.toString(accountId)); 4251 } 4252 4253 private void notifyUI(Uri uri, String id) { 4254 final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri; 4255 getContext().getContentResolver().notifyChange(notifyUri, null); 4256 } 4257 4258 private void notifyUI(Uri uri, long id) { 4259 notifyUI(uri, Long.toString(id)); 4260 } 4261 4262 private Mailbox getMailbox(final Uri uri) { 4263 final long id = Long.parseLong(uri.getLastPathSegment()); 4264 return Mailbox.restoreMailboxWithId(getContext(), id); 4265 } 4266 4267 /** 4268 * Create an android.accounts.Account object for this account. 4269 * @param accountId id of account to load. 4270 * @return an android.accounts.Account for this account, or null if we can't load it. 4271 */ 4272 private android.accounts.Account getAccountManagerAccount(final long accountId) { 4273 final Context context = getContext(); 4274 final Account account = Account.restoreAccountWithId(context, accountId); 4275 if (account == null) return null; 4276 EmailServiceInfo info = 4277 EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); 4278 return new android.accounts.Account(account.mEmailAddress, info.accountType); 4279 } 4280 4281 /** 4282 * Update an account's periodic sync if the sync interval has changed. 4283 * @param accountId id for the account to update. 4284 * @param values the ContentValues for this update to the account. 4285 */ 4286 private void updateAccountSyncInterval(final long accountId, final ContentValues values) { 4287 final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL); 4288 if (syncInterval == null) { 4289 // No change to the sync interval. 4290 return; 4291 } 4292 final android.accounts.Account account = getAccountManagerAccount(accountId); 4293 if (account == null) { 4294 // Unable to load the account, or unknown protocol. 4295 return; 4296 } 4297 4298 LogUtils.d(TAG, "Setting sync interval for account " + accountId + " to " + syncInterval + 4299 " minutes"); 4300 4301 // First remove all existing periodic syncs. 4302 final List<PeriodicSync> syncs = 4303 ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY); 4304 for (final PeriodicSync sync : syncs) { 4305 ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras); 4306 } 4307 4308 // Only positive values of sync interval indicate periodic syncs. The value is in minutes, 4309 // while addPeriodicSync expects its time in seconds. 4310 if (syncInterval > 0) { 4311 ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY, 4312 syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); 4313 } 4314 } 4315 4316 private void startSync(final Mailbox mailbox, final int deltaMessageCount) { 4317 android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey); 4318 Bundle extras = new Bundle(7); 4319 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 4320 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 4321 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 4322 extras.putLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, mailbox.mId); 4323 if (deltaMessageCount != 0) { 4324 extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); 4325 } 4326 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, 4327 EmailContent.CONTENT_URI.toString()); 4328 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, 4329 SYNC_STATUS_CALLBACK_METHOD); 4330 ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); 4331 } 4332 4333 private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { 4334 if (mailbox != null) { 4335 startSync(mailbox, deltaMessageCount); 4336 } 4337 return null; 4338 } 4339 4340 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 4341 public static final int VISIBLE_LIMIT_INCREMENT = 10; 4342 //Number of additional messages to load when a user selects "Load more..." in a search 4343 public static final int SEARCH_MORE_INCREMENT = 10; 4344 4345 private Cursor uiFolderLoadMore(final Mailbox mailbox) { 4346 if (mailbox == null) return null; 4347 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 4348 // Ask for 10 more messages 4349 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 4350 runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId); 4351 } else { 4352 uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT); 4353 } 4354 return null; 4355 } 4356 4357 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 4358 private SearchParams mSearchParams; 4359 4360 /** 4361 * Returns the search mailbox for the specified account, creating one if necessary 4362 * @return the search mailbox for the passed in account 4363 */ 4364 private Mailbox getSearchMailbox(long accountId) { 4365 Context context = getContext(); 4366 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 4367 if (m == null) { 4368 m = new Mailbox(); 4369 m.mAccountKey = accountId; 4370 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 4371 m.mFlagVisible = false; 4372 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 4373 m.mSyncInterval = 0; 4374 m.mType = Mailbox.TYPE_SEARCH; 4375 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 4376 m.mParentKey = Mailbox.NO_MAILBOX; 4377 m.save(context); 4378 } 4379 return m; 4380 } 4381 4382 private void runSearchQuery(final Context context, final long accountId, 4383 final long searchMailboxId) { 4384 LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d", 4385 accountId, searchMailboxId); 4386 4387 // Start the search running in the background 4388 new AsyncTask<Void, Void, Void>() { 4389 @Override 4390 public Void doInBackground(Void... params) { 4391 final EmailServiceProxy service = 4392 EmailServiceUtils.getServiceForAccount(context, accountId); 4393 if (service != null) { 4394 try { 4395 // Save away the total count 4396 mSearchParams.mTotalCount = 4397 service.searchMessages(accountId, mSearchParams, searchMailboxId); 4398 LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d", 4399 mSearchParams.mTotalCount); 4400 notifyUIFolder(searchMailboxId, accountId); 4401 } catch (RemoteException e) { 4402 LogUtils.e("searchMessages", "RemoteException", e); 4403 } 4404 } 4405 return null; 4406 } 4407 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 4408 } 4409 4410 // TODO: Handle searching for more... 4411 private Cursor uiSearch(Uri uri, String[] projection) { 4412 LogUtils.d(TAG, "runSearchQuery in search %s", uri); 4413 final long accountId = Long.parseLong(uri.getLastPathSegment()); 4414 4415 // TODO: Check the actual mailbox 4416 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 4417 if (inbox == null) { 4418 LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account " 4419 + accountId); 4420 4421 return null; 4422 } 4423 4424 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 4425 if (filter == null) { 4426 throw new IllegalArgumentException("No query parameter in search query"); 4427 } 4428 4429 // Find/create our search mailbox 4430 Mailbox searchMailbox = getSearchMailbox(accountId); 4431 final long searchMailboxId = searchMailbox.mId; 4432 4433 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 4434 4435 final Context context = getContext(); 4436 if (mSearchParams.mOffset == 0) { 4437 LogUtils.d(TAG, "deleting existing search results."); 4438 4439 // Delete existing contents of search mailbox 4440 ContentResolver resolver = context.getContentResolver(); 4441 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 4442 null); 4443 final ContentValues cv = new ContentValues(); 4444 // For now, use the actual query as the name of the mailbox 4445 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 4446 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 4447 cv, null, null); 4448 } 4449 4450 // Start the search running in the background 4451 runSearchQuery(context, accountId, searchMailboxId); 4452 4453 // This will look just like a "normal" folder 4454 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 4455 searchMailbox.mId), projection, false); 4456 } 4457 4458 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 4459 4460 /** 4461 * Delete an account and clean it up 4462 */ 4463 private int uiDeleteAccount(Uri uri) { 4464 Context context = getContext(); 4465 long accountId = Long.parseLong(uri.getLastPathSegment()); 4466 try { 4467 // Get the account URI. 4468 final Account account = Account.restoreAccountWithId(context, accountId); 4469 if (account == null) { 4470 return 0; // Already deleted? 4471 } 4472 4473 deleteAccountData(context, accountId); 4474 4475 // Now delete the account itself 4476 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 4477 context.getContentResolver().delete(uri, null, null); 4478 4479 // Clean up 4480 AccountBackupRestore.backup(context); 4481 SecurityPolicy.getInstance(context).reducePolicies(); 4482 MailActivityEmail.setServicesEnabledSync(context); 4483 return 1; 4484 } catch (Exception e) { 4485 LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e); 4486 } 4487 return 0; 4488 } 4489 4490 private int uiDeleteAccountData(Uri uri) { 4491 Context context = getContext(); 4492 long accountId = Long.parseLong(uri.getLastPathSegment()); 4493 // Get the account URI. 4494 final Account account = Account.restoreAccountWithId(context, accountId); 4495 if (account == null) { 4496 return 0; // Already deleted? 4497 } 4498 deleteAccountData(context, accountId); 4499 return 1; 4500 } 4501 4502 /** Projection used for getting email address for an account. */ 4503 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 4504 4505 private static void deleteAccountData(Context context, long accountId) { 4506 // We will delete PIM data, but by the time the asynchronous call to do that happens, 4507 // the account may have been deleted from the DB. Therefore we have to get the email 4508 // address now and send that, rather than the account id. 4509 final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI, 4510 ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 4511 new String[] {Long.toString(accountId)}, null, 0); 4512 if (emailAddress == null) { 4513 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 4514 } 4515 4516 // Delete synced attachments 4517 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 4518 4519 // Delete all mailboxes. 4520 ContentResolver resolver = context.getContentResolver(); 4521 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 4522 resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 4523 4524 // Delete account sync key. 4525 final ContentValues cv = new ContentValues(); 4526 cv.putNull(Account.SYNC_KEY); 4527 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 4528 4529 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 4530 if (emailAddress != null) { 4531 final IEmailService service = 4532 EmailServiceUtils.getServiceForAccount(context, accountId); 4533 if (service != null) { 4534 try { 4535 service.deleteAccountPIMData(emailAddress); 4536 } catch (final RemoteException e) { 4537 // Can't do anything about this 4538 } 4539 } 4540 } 4541 } 4542 4543 private int[] mSavedWidgetIds = new int[0]; 4544 private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 4545 private AppWidgetManager mAppWidgetManager; 4546 private ComponentName mEmailComponent; 4547 4548 private void notifyWidgets(long mailboxId) { 4549 Context context = getContext(); 4550 // Lazily initialize these 4551 if (mAppWidgetManager == null) { 4552 mAppWidgetManager = AppWidgetManager.getInstance(context); 4553 mEmailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME); 4554 } 4555 4556 // See if we have to populate our array of mailboxes used in widgets 4557 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 4558 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 4559 mSavedWidgetIds = widgetIds; 4560 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 4561 // widgetInfo now has pairs of account uri/folder uri 4562 mWidgetNotifyMailboxes.clear(); 4563 for (String[] widgetInfo: widgetInfos) { 4564 try { 4565 if (widgetInfo == null) continue; 4566 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 4567 if (!isCombinedMailbox(id)) { 4568 // For a regular mailbox, just add it to the list 4569 if (!mWidgetNotifyMailboxes.contains(id)) { 4570 mWidgetNotifyMailboxes.add(id); 4571 } 4572 } else { 4573 switch (getVirtualMailboxType(id)) { 4574 // We only handle the combined inbox in widgets 4575 case Mailbox.TYPE_INBOX: 4576 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 4577 MailboxColumns.TYPE + "=?", 4578 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 4579 try { 4580 while (c.moveToNext()) { 4581 mWidgetNotifyMailboxes.add( 4582 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 4583 } 4584 } finally { 4585 c.close(); 4586 } 4587 break; 4588 } 4589 } 4590 } catch (NumberFormatException e) { 4591 // Move along 4592 } 4593 } 4594 } 4595 4596 // If our mailbox needs to be notified, do so... 4597 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 4598 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 4599 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 4600 intent.setType(EMAIL_APP_MIME_TYPE); 4601 context.sendBroadcast(intent); 4602 } 4603 } 4604 4605 @Override 4606 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 4607 Context context = getContext(); 4608 writer.println("Installed services:"); 4609 for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) { 4610 writer.println(" " + info); 4611 } 4612 writer.println(); 4613 writer.println("Accounts: "); 4614 Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); 4615 if (cursor.getCount() == 0) { 4616 writer.println(" None"); 4617 } 4618 try { 4619 while (cursor.moveToNext()) { 4620 Account account = new Account(); 4621 account.restore(cursor); 4622 writer.println(" Account " + account.mDisplayName); 4623 HostAuth hostAuth = 4624 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 4625 if (hostAuth != null) { 4626 writer.println(" Protocol = " + hostAuth.mProtocol + 4627 (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " + 4628 account.mProtocolVersion)); 4629 } 4630 } 4631 } finally { 4632 cursor.close(); 4633 } 4634 } 4635} 4636