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