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