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