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