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