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