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