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