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