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