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