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