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