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