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