EmailProvider.java revision 8c989772dfba08438650575f1ac2bb952bd56158
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.ComponentCallbacks; 21import android.content.ComponentName; 22import android.content.ContentProvider; 23import android.content.ContentProviderOperation; 24import android.content.ContentProviderResult; 25import android.content.ContentResolver; 26import android.content.ContentUris; 27import android.content.ContentValues; 28import android.content.Context; 29import android.content.Intent; 30import android.content.OperationApplicationException; 31import android.content.PeriodicSync; 32import android.content.UriMatcher; 33import android.content.pm.ActivityInfo; 34import android.content.res.Configuration; 35import android.content.res.Resources; 36import android.database.ContentObserver; 37import android.database.Cursor; 38import android.database.CursorWrapper; 39import android.database.DatabaseUtils; 40import android.database.MatrixCursor; 41import android.database.MergeCursor; 42import android.database.sqlite.SQLiteDatabase; 43import android.database.sqlite.SQLiteException; 44import android.net.Uri; 45import android.os.AsyncTask; 46import android.os.Binder; 47import android.os.Bundle; 48import android.os.Handler; 49import android.os.Handler.Callback; 50import android.os.Looper; 51import android.os.Parcel; 52import android.os.ParcelFileDescriptor; 53import android.os.RemoteException; 54import android.provider.BaseColumns; 55import android.text.TextUtils; 56import android.text.format.DateUtils; 57import android.text.util.Rfc822Token; 58import android.text.util.Rfc822Tokenizer; 59import android.util.Base64; 60import android.util.Log; 61import android.util.SparseArray; 62 63import com.android.common.content.ProjectionMap; 64import com.android.email.Preferences; 65import com.android.email.R; 66import com.android.email.SecurityPolicy; 67import com.android.email.service.AttachmentDownloadService; 68import com.android.email.service.EmailServiceUtils; 69import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 70import com.android.email2.ui.MailActivityEmail; 71import com.android.emailcommon.Logging; 72import com.android.emailcommon.mail.Address; 73import com.android.emailcommon.provider.Account; 74import com.android.emailcommon.provider.EmailContent; 75import com.android.emailcommon.provider.EmailContent.AccountColumns; 76import com.android.emailcommon.provider.EmailContent.Attachment; 77import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 78import com.android.emailcommon.provider.EmailContent.Body; 79import com.android.emailcommon.provider.EmailContent.BodyColumns; 80import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 81import com.android.emailcommon.provider.EmailContent.MailboxColumns; 82import com.android.emailcommon.provider.EmailContent.Message; 83import com.android.emailcommon.provider.EmailContent.MessageColumns; 84import com.android.emailcommon.provider.EmailContent.PolicyColumns; 85import com.android.emailcommon.provider.EmailContent.SyncColumns; 86import com.android.emailcommon.provider.HostAuth; 87import com.android.emailcommon.provider.Mailbox; 88import com.android.emailcommon.provider.MailboxUtilities; 89import com.android.emailcommon.provider.MessageChangeLogTable; 90import com.android.emailcommon.provider.MessageMove; 91import com.android.emailcommon.provider.MessageStateChange; 92import com.android.emailcommon.provider.Policy; 93import com.android.emailcommon.provider.QuickResponse; 94import com.android.emailcommon.service.EmailServiceProxy; 95import com.android.emailcommon.service.EmailServiceStatus; 96import com.android.emailcommon.service.IEmailService; 97import com.android.emailcommon.service.SearchParams; 98import com.android.emailcommon.utility.AttachmentUtilities; 99import com.android.emailcommon.utility.Utility; 100import com.android.ex.photo.provider.PhotoContract; 101import com.android.mail.preferences.MailPrefs; 102import com.android.mail.providers.ConversationInfo; 103import com.android.mail.providers.Folder; 104import com.android.mail.providers.FolderList; 105import com.android.mail.providers.MessageInfo; 106import com.android.mail.providers.UIProvider; 107import com.android.mail.providers.UIProvider.AccountCapabilities; 108import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 109import com.android.mail.providers.UIProvider.ConversationColumns; 110import com.android.mail.providers.UIProvider.ConversationPriority; 111import com.android.mail.providers.UIProvider.ConversationSendingState; 112import com.android.mail.providers.UIProvider.DraftType; 113import com.android.mail.utils.AttachmentUtils; 114import com.android.mail.utils.LogTag; 115import com.android.mail.utils.LogUtils; 116import com.android.mail.utils.MatrixCursorWithCachedColumns; 117import com.android.mail.utils.MatrixCursorWithExtra; 118import com.android.mail.utils.MimeType; 119import com.android.mail.utils.Utils; 120import com.android.mail.widget.BaseWidgetProvider; 121import com.google.common.collect.ImmutableMap; 122import com.google.common.collect.ImmutableSet; 123import com.google.common.collect.Lists; 124 125import java.io.File; 126import java.io.FileDescriptor; 127import java.io.FileNotFoundException; 128import java.io.PrintWriter; 129import java.util.ArrayList; 130import java.util.Arrays; 131import java.util.HashSet; 132import java.util.List; 133import java.util.Locale; 134import java.util.Map; 135import java.util.Set; 136import java.util.regex.Pattern; 137 138/** 139 * @author mblank 140 * 141 */ 142public class EmailProvider extends ContentProvider { 143 144 private static final String TAG = LogTag.getLogTag(); 145 146 // Time to delay upsync requests. 147 public static final long SYNC_DELAY_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS; 148 149 public static String EMAIL_APP_MIME_TYPE; 150 151 private static final String DATABASE_NAME = "EmailProvider.db"; 152 private static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; 153 private static final String BACKUP_DATABASE_NAME = "EmailProviderBackup.db"; 154 155 /** 156 * Notifies that changes happened. Certain UI components, e.g., widgets, can register for this 157 * {@link android.content.Intent} and update accordingly. However, this can be very broad and 158 * is NOT the preferred way of getting notification. 159 */ 160 private static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = 161 "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; 162 163 private static final String EMAIL_MESSAGE_MIME_TYPE = 164 "vnd.android.cursor.item/email-message"; 165 private static final String EMAIL_ATTACHMENT_MIME_TYPE = 166 "vnd.android.cursor.item/email-attachment"; 167 168 /** Appended to the notification URI for delete operations */ 169 private static final String NOTIFICATION_OP_DELETE = "delete"; 170 /** Appended to the notification URI for insert operations */ 171 private static final String NOTIFICATION_OP_INSERT = "insert"; 172 /** Appended to the notification URI for update operations */ 173 private static final String NOTIFICATION_OP_UPDATE = "update"; 174 175 /** The query string to trigger a folder refresh. */ 176 private static String QUERY_UIREFRESH = "uirefresh"; 177 178 // Definitions for our queries looking for orphaned messages 179 private static final String[] ORPHANS_PROJECTION 180 = new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY}; 181 private static final int ORPHANS_ID = 0; 182 private static final int ORPHANS_MAILBOX_KEY = 1; 183 184 private static final String WHERE_ID = EmailContent.RECORD_ID + "=?"; 185 186 private static final int ACCOUNT_BASE = 0; 187 private static final int ACCOUNT = ACCOUNT_BASE; 188 private static final int ACCOUNT_ID = ACCOUNT_BASE + 1; 189 private static final int ACCOUNT_RESET_NEW_COUNT = ACCOUNT_BASE + 2; 190 private static final int ACCOUNT_RESET_NEW_COUNT_ID = ACCOUNT_BASE + 3; 191 private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 4; 192 private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 5; 193 private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 6; 194 195 private static final int MAILBOX_BASE = 0x1000; 196 private static final int MAILBOX = MAILBOX_BASE; 197 private static final int MAILBOX_ID = MAILBOX_BASE + 1; 198 private static final int MAILBOX_NOTIFICATION = MAILBOX_BASE + 2; 199 private static final int MAILBOX_MOST_RECENT_MESSAGE = MAILBOX_BASE + 3; 200 private static final int MAILBOX_MESSAGE_COUNT = MAILBOX_BASE + 4; 201 202 private static final int MESSAGE_BASE = 0x2000; 203 private static final int MESSAGE = MESSAGE_BASE; 204 private static final int MESSAGE_ID = MESSAGE_BASE + 1; 205 private static final int SYNCED_MESSAGE_ID = MESSAGE_BASE + 2; 206 private static final int MESSAGE_SELECTION = MESSAGE_BASE + 3; 207 private static final int MESSAGE_MOVE = MESSAGE_BASE + 4; 208 private static final int MESSAGE_STATE_CHANGE = MESSAGE_BASE + 5; 209 210 private static final int ATTACHMENT_BASE = 0x3000; 211 private static final int ATTACHMENT = ATTACHMENT_BASE; 212 private static final int ATTACHMENT_ID = ATTACHMENT_BASE + 1; 213 private static final int ATTACHMENTS_MESSAGE_ID = ATTACHMENT_BASE + 2; 214 private static final int ATTACHMENTS_CACHED_FILE_ACCESS = ATTACHMENT_BASE + 3; 215 216 private static final int HOSTAUTH_BASE = 0x4000; 217 private static final int HOSTAUTH = HOSTAUTH_BASE; 218 private static final int HOSTAUTH_ID = HOSTAUTH_BASE + 1; 219 220 private static final int UPDATED_MESSAGE_BASE = 0x5000; 221 private static final int UPDATED_MESSAGE = UPDATED_MESSAGE_BASE; 222 private static final int UPDATED_MESSAGE_ID = UPDATED_MESSAGE_BASE + 1; 223 224 private static final int DELETED_MESSAGE_BASE = 0x6000; 225 private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; 226 private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; 227 228 private static final int POLICY_BASE = 0x7000; 229 private static final int POLICY = POLICY_BASE; 230 private static final int POLICY_ID = POLICY_BASE + 1; 231 232 private static final int QUICK_RESPONSE_BASE = 0x8000; 233 private static final int QUICK_RESPONSE = QUICK_RESPONSE_BASE; 234 private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; 235 private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; 236 237 private static final int UI_BASE = 0x9000; 238 private static final int UI_FOLDERS = UI_BASE; 239 private static final int UI_SUBFOLDERS = UI_BASE + 1; 240 private static final int UI_MESSAGES = UI_BASE + 2; 241 private static final int UI_MESSAGE = UI_BASE + 3; 242 private static final int UI_UNDO = UI_BASE + 4; 243 private static final int UI_FOLDER_REFRESH = UI_BASE + 5; 244 private static final int UI_FOLDER = UI_BASE + 6; 245 private static final int UI_ACCOUNT = UI_BASE + 7; 246 private static final int UI_ACCTS = UI_BASE + 8; 247 private static final int UI_ATTACHMENTS = UI_BASE + 9; 248 private static final int UI_ATTACHMENT = UI_BASE + 10; 249 private static final int UI_SEARCH = UI_BASE + 11; 250 private static final int UI_ACCOUNT_DATA = UI_BASE + 12; 251 private static final int UI_FOLDER_LOAD_MORE = UI_BASE + 13; 252 private static final int UI_CONVERSATION = UI_BASE + 14; 253 private static final int UI_RECENT_FOLDERS = UI_BASE + 15; 254 private static final int UI_DEFAULT_RECENT_FOLDERS = UI_BASE + 16; 255 private static final int UI_FULL_FOLDERS = UI_BASE + 17; 256 private static final int UI_ALL_FOLDERS = UI_BASE + 18; 257 258 private static final int BODY_BASE = 0xA000; 259 private static final int BODY = BODY_BASE; 260 private static final int BODY_ID = BODY_BASE + 1; 261 262 private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. 263 264 private static final SparseArray<String> TABLE_NAMES; 265 static { 266 SparseArray<String> array = new SparseArray<String>(11); 267 array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME); 268 array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME); 269 array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME); 270 array.put(ATTACHMENT_BASE >> BASE_SHIFT, Attachment.TABLE_NAME); 271 array.put(HOSTAUTH_BASE >> BASE_SHIFT, HostAuth.TABLE_NAME); 272 array.put(UPDATED_MESSAGE_BASE >> BASE_SHIFT, Message.UPDATED_TABLE_NAME); 273 array.put(DELETED_MESSAGE_BASE >> BASE_SHIFT, Message.DELETED_TABLE_NAME); 274 array.put(POLICY_BASE >> BASE_SHIFT, Policy.TABLE_NAME); 275 array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME); 276 array.put(UI_BASE >> BASE_SHIFT, null); 277 array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); 278 TABLE_NAMES = array; 279 } 280 281 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 282 283 /** 284 * Functions which manipulate the database connection or files synchronize on this. 285 * It's static because there can be multiple provider objects. 286 * TODO: Do we actually need to synchronize across all DB access, not just connection creation? 287 */ 288 private static final Object sDatabaseLock = new Object(); 289 290 /** 291 * Let's only generate these SQL strings once, as they are used frequently 292 * Note that this isn't relevant for table creation strings, since they are used only once 293 */ 294 private static final String UPDATED_MESSAGE_INSERT = "insert or ignore into " + 295 Message.UPDATED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 296 EmailContent.RECORD_ID + '='; 297 298 private static final String UPDATED_MESSAGE_DELETE = "delete from " + 299 Message.UPDATED_TABLE_NAME + " where " + EmailContent.RECORD_ID + '='; 300 301 private static final String DELETED_MESSAGE_INSERT = "insert or replace into " + 302 Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " + 303 EmailContent.RECORD_ID + '='; 304 305 private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME + 306 " where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY + 307 " from " + Body.TABLE_NAME + " except select " + EmailContent.RECORD_ID + " from " + 308 Message.TABLE_NAME + ')'; 309 310 private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME + 311 " where " + BodyColumns.MESSAGE_KEY + '='; 312 313 private static ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 314 private static final ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); 315 316 private static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; 317 318 // For undo handling 319 private int mLastSequence = -1; 320 private final ArrayList<ContentProviderOperation> mLastSequenceOps = 321 new ArrayList<ContentProviderOperation>(); 322 323 // Query parameter indicating the command came from UIProvider 324 private static final String IS_UIPROVIDER = "is_uiprovider"; 325 326 private static final String SYNC_STATUS_CALLBACK_METHOD = "sync_status"; 327 328 /** 329 * Wrap the UriMatcher call so we can throw a runtime exception if an unknown Uri is passed in 330 * @param uri the Uri to match 331 * @return the match value 332 */ 333 private static int findMatch(Uri uri, String methodName) { 334 int match = sURIMatcher.match(uri); 335 if (match < 0) { 336 throw new IllegalArgumentException("Unknown uri: " + uri); 337 } else if (Logging.LOGD) { 338 LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match); 339 } 340 return match; 341 } 342 343 private static Uri INTEGRITY_CHECK_URI; 344 public static Uri ACCOUNT_BACKUP_URI; 345 private static Uri FOLDER_STATUS_URI; 346 347 private SQLiteDatabase mDatabase; 348 private SQLiteDatabase mBodyDatabase; 349 350 private Handler mDelayedSyncHandler; 351 private final Set<SyncRequestMessage> mDelayedSyncRequests = new HashSet<SyncRequestMessage>(); 352 353 public static Uri uiUri(String type, long id) { 354 return Uri.parse(uiUriString(type, id)); 355 } 356 357 /** 358 * Creates a URI string from a database ID (guaranteed to be unique). 359 * @param type of the resource: uifolder, message, etc. 360 * @param id the id of the resource. 361 * @return uri string 362 */ 363 public static String uiUriString(String type, long id) { 364 return "content://" + EmailContent.AUTHORITY + "/" + type + ((id == -1) ? "" : ("/" + id)); 365 } 366 367 /** 368 * Orphan record deletion utility. Generates a sqlite statement like: 369 * delete from <table> where <column> not in (select <foreignColumn> from <foreignTable>) 370 * @param db the EmailProvider database 371 * @param table the table whose orphans are to be removed 372 * @param column the column deletion will be based on 373 * @param foreignColumn the column in the foreign table whose absence will trigger the deletion 374 * @param foreignTable the foreign table 375 */ 376 private static void deleteUnlinked(SQLiteDatabase db, String table, String column, 377 String foreignColumn, String foreignTable) { 378 int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + 379 foreignTable + ")", null); 380 if (count > 0) { 381 LogUtils.w(TAG, "Found " + count + " orphaned row(s) in " + table); 382 } 383 } 384 385 386 /** 387 * Make sure that parentKeys match with parentServerId. 388 * When we sync folders, we do two passes: First to create the mailbox rows, and second 389 * to set the parentKeys. Two passes are needed because we won't know the parent's Id 390 * until that row is inserted, and the order in which the rows are given is arbitrary. 391 * If we crash while this operation is in progress, the parent keys can be left uninitialized. 392 * @param db 393 */ 394 private void fixParentKeys(SQLiteDatabase db) { 395 LogUtils.d(TAG, "Fixing parent keys"); 396 397 // Update the parentKey for each mailbox row to match the _id of the row whose 398 // serverId matches our parentServerId. This will leave parentKey blank for any 399 // row that does not have a parentServerId 400 401 // This is kind of a confusing sql statement, so here's the actual text of it, 402 // for reference: 403 // 404 // update mailbox set parentKey = (select _id from mailbox as b where 405 // mailbox.parentServerId=b.serverId and mailbox.parentServerId not null and 406 // mailbox.accountKey=b.accountKey) 407 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY + "=" 408 + "(select " + Mailbox.ID + " from " + Mailbox.TABLE_NAME + " as b where " 409 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + "=" 410 + "b." + MailboxColumns.SERVER_ID + " and " 411 + Mailbox.TABLE_NAME + "." + MailboxColumns.PARENT_SERVER_ID + " not null and " 412 + Mailbox.TABLE_NAME + "." + MailboxColumns.ACCOUNT_KEY 413 + "=b." + Mailbox.ACCOUNT_KEY + ")"); 414 415 // Top level folders can still have uninitialized parent keys. Update these 416 // to indicate that the parent is -1. 417 // 418 // update mailbox set parentKey = -1 where parentKey=0 or parentKey is null; 419 db.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.PARENT_KEY 420 + "=" + Mailbox.NO_MAILBOX + " where " + MailboxColumns.PARENT_KEY 421 + "=" + Mailbox.PARENT_KEY_UNINITIALIZED + " or " + MailboxColumns.PARENT_KEY 422 + " is null"); 423 424 } 425 426 427 private SQLiteDatabase getDatabase(Context context) { 428 synchronized (sDatabaseLock) { 429 // Always return the cached database, if we've got one 430 if (mDatabase != null) { 431 return mDatabase; 432 } 433 434 // Whenever we create or re-cache the databases, make sure that we haven't lost one 435 // to corruption 436 checkDatabases(); 437 438 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, DATABASE_NAME); 439 mDatabase = helper.getWritableDatabase(); 440 DBHelper.BodyDatabaseHelper bodyHelper = 441 new DBHelper.BodyDatabaseHelper(context, BODY_DATABASE_NAME); 442 mBodyDatabase = bodyHelper.getWritableDatabase(); 443 if (mBodyDatabase != null) { 444 String bodyFileName = mBodyDatabase.getPath(); 445 mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); 446 } 447 448 // Restore accounts if the database is corrupted... 449 restoreIfNeeded(context, mDatabase); 450 // Check for any orphaned Messages in the updated/deleted tables 451 deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); 452 deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); 453 // Delete orphaned mailboxes/messages/policies (account no longer exists) 454 deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, 455 AccountColumns.ID, Account.TABLE_NAME); 456 deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, 457 AccountColumns.ID, Account.TABLE_NAME); 458 deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, 459 AccountColumns.POLICY_KEY, Account.TABLE_NAME); 460 fixParentKeys(mDatabase); 461 initUiProvider(); 462 return mDatabase; 463 } 464 } 465 466 /** 467 * Perform startup actions related to UI 468 */ 469 private void initUiProvider() { 470 // Clear mailbox sync status 471 mDatabase.execSQL("update " + Mailbox.TABLE_NAME + " set " + MailboxColumns.UI_SYNC_STATUS + 472 "=" + UIProvider.SyncStatus.NO_SYNC); 473 } 474 475 /** 476 * Restore user Account and HostAuth data from our backup database 477 */ 478 private static void restoreIfNeeded(Context context, SQLiteDatabase mainDatabase) { 479 if (MailActivityEmail.DEBUG) { 480 LogUtils.w(TAG, "restoreIfNeeded..."); 481 } 482 // Check for legacy backup 483 String legacyBackup = Preferences.getLegacyBackupPreference(context); 484 // If there's a legacy backup, create a new-style backup and delete the legacy backup 485 // In the 1:1000000000 chance that the user gets an app update just as his database becomes 486 // corrupt, oh well... 487 if (!TextUtils.isEmpty(legacyBackup)) { 488 backupAccounts(context, mainDatabase); 489 Preferences.clearLegacyBackupPreference(context); 490 LogUtils.w(TAG, "Created new EmailProvider backup database"); 491 return; 492 } 493 494 // If we have accounts, we're done 495 if (DatabaseUtils.longForQuery(mainDatabase, 496 "SELECT EXISTS (SELECT ? FROM " + Account.TABLE_NAME + " )", 497 EmailContent.ID_PROJECTION) > 0) { 498 if (MailActivityEmail.DEBUG) { 499 LogUtils.w(TAG, "restoreIfNeeded: Account exists."); 500 } 501 return; 502 } 503 504 restoreAccounts(context, mainDatabase); 505 } 506 507 /** {@inheritDoc} */ 508 @Override 509 public void shutdown() { 510 if (mDatabase != null) { 511 mDatabase.close(); 512 mDatabase = null; 513 } 514 if (mBodyDatabase != null) { 515 mBodyDatabase.close(); 516 mBodyDatabase = null; 517 } 518 } 519 520 private static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { 521 if (database != null) { 522 // We'll look at all of the items in the table; there won't be many typically 523 Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); 524 // Usually, there will be nothing in these tables, so make a quick check 525 try { 526 if (c.getCount() == 0) return; 527 ArrayList<Long> foundMailboxes = new ArrayList<Long>(); 528 ArrayList<Long> notFoundMailboxes = new ArrayList<Long>(); 529 ArrayList<Long> deleteList = new ArrayList<Long>(); 530 String[] bindArray = new String[1]; 531 while (c.moveToNext()) { 532 // Get the mailbox key and see if we've already found this mailbox 533 // If so, we're fine 534 long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY); 535 // If we already know this mailbox doesn't exist, mark the message for deletion 536 if (notFoundMailboxes.contains(mailboxId)) { 537 deleteList.add(c.getLong(ORPHANS_ID)); 538 // If we don't know about this mailbox, we'll try to find it 539 } else if (!foundMailboxes.contains(mailboxId)) { 540 bindArray[0] = Long.toString(mailboxId); 541 Cursor boxCursor = database.query(Mailbox.TABLE_NAME, 542 Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null); 543 try { 544 // If it exists, we'll add it to the "found" mailboxes 545 if (boxCursor.moveToFirst()) { 546 foundMailboxes.add(mailboxId); 547 // Otherwise, we'll add to "not found" and mark the message for deletion 548 } else { 549 notFoundMailboxes.add(mailboxId); 550 deleteList.add(c.getLong(ORPHANS_ID)); 551 } 552 } finally { 553 boxCursor.close(); 554 } 555 } 556 } 557 // Now, delete the orphan messages 558 for (long messageId: deleteList) { 559 bindArray[0] = Long.toString(messageId); 560 database.delete(tableName, WHERE_ID, bindArray); 561 } 562 } finally { 563 c.close(); 564 } 565 } 566 } 567 568 @Override 569 public int delete(Uri uri, String selection, String[] selectionArgs) { 570 Log.d(TAG, "Delete: " + uri); 571 final int match = findMatch(uri, "delete"); 572 Context context = getContext(); 573 // Pick the correct database for this operation 574 // If we're in a transaction already (which would happen during applyBatch), then the 575 // body database is already attached to the email database and any attempt to use the 576 // body database directly will result in a SQLiteException (the database is locked) 577 SQLiteDatabase db = getDatabase(context); 578 int table = match >> BASE_SHIFT; 579 String id = "0"; 580 boolean messageDeletion = false; 581 ContentResolver resolver = context.getContentResolver(); 582 583 String tableName = TABLE_NAMES.valueAt(table); 584 int result = -1; 585 586 try { 587 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 588 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 589 notifyUIConversation(uri); 590 } 591 } 592 switch (match) { 593 case UI_MESSAGE: 594 return uiDeleteMessage(uri); 595 case UI_ACCOUNT_DATA: 596 return uiDeleteAccountData(uri); 597 case UI_ACCOUNT: 598 return uiDeleteAccount(uri); 599 case MESSAGE_SELECTION: 600 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 601 selectionArgs, null, null, null); 602 try { 603 if (findCursor.moveToFirst()) { 604 return delete(ContentUris.withAppendedId( 605 Message.CONTENT_URI, 606 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 607 null, null); 608 } else { 609 return 0; 610 } 611 } finally { 612 findCursor.close(); 613 } 614 // These are cases in which one or more Messages might get deleted, either by 615 // cascade or explicitly 616 case MAILBOX_ID: 617 case MAILBOX: 618 case ACCOUNT_ID: 619 case ACCOUNT: 620 case MESSAGE: 621 case SYNCED_MESSAGE_ID: 622 case MESSAGE_ID: 623 // Handle lost Body records here, since this cannot be done in a trigger 624 // The process is: 625 // 1) Begin a transaction, ensuring that both databases are affected atomically 626 // 2) Do the requested deletion, with cascading deletions handled in triggers 627 // 3) End the transaction, committing all changes atomically 628 // 629 // Bodies are auto-deleted here; Attachments are auto-deleted via trigger 630 messageDeletion = true; 631 db.beginTransaction(); 632 break; 633 } 634 switch (match) { 635 case BODY_ID: 636 case DELETED_MESSAGE_ID: 637 case SYNCED_MESSAGE_ID: 638 case MESSAGE_ID: 639 case UPDATED_MESSAGE_ID: 640 case ATTACHMENT_ID: 641 case MAILBOX_ID: 642 case ACCOUNT_ID: 643 case HOSTAUTH_ID: 644 case POLICY_ID: 645 case QUICK_RESPONSE_ID: 646 id = uri.getPathSegments().get(1); 647 if (match == SYNCED_MESSAGE_ID) { 648 // For synced messages, first copy the old message to the deleted table and 649 // delete it from the updated table (in case it was updated first) 650 // Note that this is all within a transaction, for atomicity 651 db.execSQL(DELETED_MESSAGE_INSERT + id); 652 db.execSQL(UPDATED_MESSAGE_DELETE + id); 653 } 654 655 final long accountId; 656 if (match == MAILBOX_ID) { 657 accountId = Mailbox.getAccountIdForMailbox(context, id); 658 } else { 659 accountId = Account.NO_ACCOUNT; 660 } 661 662 result = db.delete(tableName, whereWithId(id, selection), selectionArgs); 663 664 if (match == ACCOUNT_ID) { 665 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 666 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 667 } else if (match == MAILBOX_ID) { 668 notifyUIFolder(id, accountId); 669 } else if (match == ATTACHMENT_ID) { 670 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 671 } 672 break; 673 case ATTACHMENTS_MESSAGE_ID: 674 // All attachments for the given message 675 id = uri.getPathSegments().get(2); 676 result = db.delete(tableName, 677 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs); 678 break; 679 680 case BODY: 681 case MESSAGE: 682 case DELETED_MESSAGE: 683 case UPDATED_MESSAGE: 684 case ATTACHMENT: 685 case MAILBOX: 686 case ACCOUNT: 687 case HOSTAUTH: 688 case POLICY: 689 result = db.delete(tableName, selection, selectionArgs); 690 break; 691 case MESSAGE_MOVE: 692 db.delete(MessageMove.TABLE_NAME, selection, selectionArgs); 693 break; 694 case MESSAGE_STATE_CHANGE: 695 db.delete(MessageStateChange.TABLE_NAME, selection, selectionArgs); 696 break; 697 default: 698 throw new IllegalArgumentException("Unknown URI " + uri); 699 } 700 if (messageDeletion) { 701 if (match == MESSAGE_ID) { 702 // Delete the Body record associated with the deleted message 703 db.execSQL(DELETE_BODY + id); 704 } else { 705 // Delete any orphaned Body records 706 db.execSQL(DELETE_ORPHAN_BODIES); 707 } 708 db.setTransactionSuccessful(); 709 } 710 } catch (SQLiteException e) { 711 checkDatabases(); 712 throw e; 713 } finally { 714 if (messageDeletion) { 715 db.endTransaction(); 716 } 717 } 718 719 // Notify all notifier cursors 720 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); 721 722 // Notify all email content cursors 723 resolver.notifyChange(EmailContent.CONTENT_URI, null); 724 return result; 725 } 726 727 @Override 728 // Use the email- prefix because message, mailbox, and account are so generic (e.g. SMS, IM) 729 public String getType(Uri uri) { 730 int match = findMatch(uri, "getType"); 731 switch (match) { 732 case BODY_ID: 733 return "vnd.android.cursor.item/email-body"; 734 case BODY: 735 return "vnd.android.cursor.dir/email-body"; 736 case UPDATED_MESSAGE_ID: 737 case MESSAGE_ID: 738 // NOTE: According to the framework folks, we're supposed to invent mime types as 739 // a way of passing information to drag & drop recipients. 740 // If there's a mailboxId parameter in the url, we respond with a mime type that 741 // has -n appended, where n is the mailboxId of the message. The drag & drop code 742 // uses this information to know not to allow dragging the item to its own mailbox 743 String mimeType = EMAIL_MESSAGE_MIME_TYPE; 744 String mailboxId = uri.getQueryParameter(MESSAGE_URI_PARAMETER_MAILBOX_ID); 745 if (mailboxId != null) { 746 mimeType += "-" + mailboxId; 747 } 748 return mimeType; 749 case UPDATED_MESSAGE: 750 case MESSAGE: 751 return "vnd.android.cursor.dir/email-message"; 752 case MAILBOX: 753 return "vnd.android.cursor.dir/email-mailbox"; 754 case MAILBOX_ID: 755 return "vnd.android.cursor.item/email-mailbox"; 756 case ACCOUNT: 757 return "vnd.android.cursor.dir/email-account"; 758 case ACCOUNT_ID: 759 return "vnd.android.cursor.item/email-account"; 760 case ATTACHMENTS_MESSAGE_ID: 761 case ATTACHMENT: 762 return "vnd.android.cursor.dir/email-attachment"; 763 case ATTACHMENT_ID: 764 return EMAIL_ATTACHMENT_MIME_TYPE; 765 case HOSTAUTH: 766 return "vnd.android.cursor.dir/email-hostauth"; 767 case HOSTAUTH_ID: 768 return "vnd.android.cursor.item/email-hostauth"; 769 default: 770 return null; 771 } 772 } 773 774 // These URIs are used for specific UI notifications. We don't use EmailContent.CONTENT_URI 775 // as the base because that gets spammed. 776 // These can't be statically initialized because they depend on EmailContent.AUTHORITY 777 private static Uri UIPROVIDER_CONVERSATION_NOTIFIER; 778 private static Uri UIPROVIDER_FOLDER_NOTIFIER; 779 private static Uri UIPROVIDER_FOLDERLIST_NOTIFIER; 780 private static Uri UIPROVIDER_ACCOUNT_NOTIFIER; 781 // Not currently used 782 //public static Uri UIPROVIDER_SETTINGS_NOTIFIER; 783 private static Uri UIPROVIDER_ATTACHMENT_NOTIFIER; 784 private static Uri UIPROVIDER_ATTACHMENTS_NOTIFIER; 785 public static Uri UIPROVIDER_ALL_ACCOUNTS_NOTIFIER; 786 private static Uri UIPROVIDER_MESSAGE_NOTIFIER; 787 private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER; 788 789 @Override 790 public Uri insert(Uri uri, ContentValues values) { 791 Log.d(TAG, "Insert: " + uri); 792 int match = findMatch(uri, "insert"); 793 Context context = getContext(); 794 ContentResolver resolver = context.getContentResolver(); 795 796 // See the comment at delete(), above 797 SQLiteDatabase db = getDatabase(context); 798 int table = match >> BASE_SHIFT; 799 String id = "0"; 800 long longId; 801 802 // We do NOT allow setting of unreadCount/messageCount via the provider 803 // These columns are maintained via triggers 804 if (match == MAILBOX_ID || match == MAILBOX) { 805 values.put(MailboxColumns.UNREAD_COUNT, 0); 806 values.put(MailboxColumns.MESSAGE_COUNT, 0); 807 } 808 809 final Uri resultUri; 810 811 try { 812 switch (match) { 813 // NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE 814 // or DELETED_MESSAGE; see the comment below for details 815 case UPDATED_MESSAGE: 816 case DELETED_MESSAGE: 817 case MESSAGE: 818 case BODY: 819 case ATTACHMENT: 820 case MAILBOX: 821 case ACCOUNT: 822 case HOSTAUTH: 823 case POLICY: 824 case QUICK_RESPONSE: 825 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 826 resultUri = ContentUris.withAppendedId(uri, longId); 827 switch(match) { 828 case MESSAGE: 829 final long mailboxId = values.getAsLong(Message.MAILBOX_KEY); 830 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 831 notifyUIConversationMailbox(mailboxId); 832 } 833 notifyUIFolder(mailboxId, values.getAsLong(Message.ACCOUNT_KEY)); 834 break; 835 case MAILBOX: 836 if (values.containsKey(MailboxColumns.TYPE)) { 837 if (values.getAsInteger(MailboxColumns.TYPE) < 838 Mailbox.TYPE_NOT_EMAIL) { 839 // Notify the account when a new mailbox is added 840 final Long accountId = 841 values.getAsLong(MailboxColumns.ACCOUNT_KEY); 842 if (accountId != null && accountId > 0) { 843 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, accountId); 844 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 845 } 846 } 847 } 848 break; 849 case ACCOUNT: 850 updateAccountSyncInterval(longId, values); 851 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 852 notifyUIAccount(longId); 853 } 854 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 855 break; 856 case UPDATED_MESSAGE: 857 case DELETED_MESSAGE: 858 throw new IllegalArgumentException("Unknown URL " + uri); 859 case ATTACHMENT: 860 int flags = 0; 861 if (values.containsKey(Attachment.FLAGS)) { 862 flags = values.getAsInteger(Attachment.FLAGS); 863 } 864 // Report all new attachments to the download service 865 mAttachmentService.attachmentChanged(getContext(), longId, flags); 866 break; 867 } 868 break; 869 case QUICK_RESPONSE_ACCOUNT_ID: 870 longId = Long.parseLong(uri.getPathSegments().get(2)); 871 values.put(EmailContent.QuickResponseColumns.ACCOUNT_KEY, longId); 872 return insert(QuickResponse.CONTENT_URI, values); 873 case MAILBOX_ID: 874 // This implies adding a message to a mailbox 875 // Hmm, a problem here is that we can't link the account as well, so it must be 876 // already in the values... 877 longId = Long.parseLong(uri.getPathSegments().get(1)); 878 values.put(MessageColumns.MAILBOX_KEY, longId); 879 return insert(Message.CONTENT_URI, values); // Recurse 880 case MESSAGE_ID: 881 // This implies adding an attachment to a message. 882 id = uri.getPathSegments().get(1); 883 longId = Long.parseLong(id); 884 values.put(AttachmentColumns.MESSAGE_KEY, longId); 885 return insert(Attachment.CONTENT_URI, values); // Recurse 886 case ACCOUNT_ID: 887 // This implies adding a mailbox to an account. 888 longId = Long.parseLong(uri.getPathSegments().get(1)); 889 values.put(MailboxColumns.ACCOUNT_KEY, longId); 890 return insert(Mailbox.CONTENT_URI, values); // Recurse 891 case ATTACHMENTS_MESSAGE_ID: 892 longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); 893 resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, longId); 894 break; 895 default: 896 throw new IllegalArgumentException("Unknown URL " + uri); 897 } 898 } catch (SQLiteException e) { 899 checkDatabases(); 900 throw e; 901 } 902 903 // Notify all notifier cursors 904 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); 905 906 // Notify all existing cursors. 907 resolver.notifyChange(EmailContent.CONTENT_URI, null); 908 return resultUri; 909 } 910 911 @Override 912 public boolean onCreate() { 913 Context context = getContext(); 914 EmailContent.init(context); 915 init(context); 916 // Do this last, so that EmailContent/EmailProvider are initialized 917 MailActivityEmail.setServicesEnabledAsync(context); 918 919 // Update widgets 920 final Intent updateAllWidgetsIntent = 921 new Intent(com.android.mail.utils.Utils.ACTION_NOTIFY_DATASET_CHANGED); 922 updateAllWidgetsIntent.putExtra(BaseWidgetProvider.EXTRA_UPDATE_ALL_WIDGETS, true); 923 updateAllWidgetsIntent.setType(context.getString(R.string.application_mime_type)); 924 context.sendBroadcast(updateAllWidgetsIntent); 925 926 // The combined account name changes on locale changes 927 final Configuration oldConfiguration = 928 new Configuration(context.getResources().getConfiguration()); 929 context.registerComponentCallbacks(new ComponentCallbacks() { 930 @Override 931 public void onConfigurationChanged(Configuration configuration) { 932 int delta = oldConfiguration.updateFrom(configuration); 933 if (Configuration.needNewResources(delta, ActivityInfo.CONFIG_LOCALE)) { 934 notifyUIAccount(COMBINED_ACCOUNT_ID); 935 } 936 } 937 938 @Override 939 public void onLowMemory() {} 940 }); 941 942 return false; 943 } 944 945 private static void init(final Context context) { 946 // Synchronize on the matcher rather than the class object to minimize risk of contention 947 // & deadlock. 948 synchronized (sURIMatcher) { 949 // We use the existence of this variable as indicative of whether this function has 950 // already run. 951 if (INTEGRITY_CHECK_URI != null) { 952 return; 953 } 954 INTEGRITY_CHECK_URI = Uri.parse("content://" + EmailContent.AUTHORITY + 955 "/integrityCheck"); 956 ACCOUNT_BACKUP_URI = 957 Uri.parse("content://" + EmailContent.AUTHORITY + "/accountBackup"); 958 FOLDER_STATUS_URI = 959 Uri.parse("content://" + EmailContent.AUTHORITY + "/status"); 960 EMAIL_APP_MIME_TYPE = context.getString(R.string.application_mime_type); 961 962 final String uiNotificationAuthority = 963 EmailContent.EMAIL_PACKAGE_NAME + ".uinotifications"; 964 UIPROVIDER_CONVERSATION_NOTIFIER = 965 Uri.parse("content://" + uiNotificationAuthority + "/uimessages"); 966 UIPROVIDER_FOLDER_NOTIFIER = 967 Uri.parse("content://" + uiNotificationAuthority + "/uifolder"); 968 UIPROVIDER_FOLDERLIST_NOTIFIER = 969 Uri.parse("content://" + uiNotificationAuthority + "/uifolders"); 970 UIPROVIDER_ACCOUNT_NOTIFIER = 971 Uri.parse("content://" + uiNotificationAuthority + "/uiaccount"); 972 // Not currently used 973 /* UIPROVIDER_SETTINGS_NOTIFIER = 974 Uri.parse("content://" + uiNotificationAuthority + "/uisettings");*/ 975 UIPROVIDER_ATTACHMENT_NOTIFIER = 976 Uri.parse("content://" + uiNotificationAuthority + "/uiattachment"); 977 UIPROVIDER_ATTACHMENTS_NOTIFIER = 978 Uri.parse("content://" + uiNotificationAuthority + "/uiattachments"); 979 UIPROVIDER_ALL_ACCOUNTS_NOTIFIER = 980 Uri.parse("content://" + uiNotificationAuthority + "/uiaccts"); 981 UIPROVIDER_MESSAGE_NOTIFIER = 982 Uri.parse("content://" + uiNotificationAuthority + "/uimessage"); 983 UIPROVIDER_RECENT_FOLDERS_NOTIFIER = 984 Uri.parse("content://" + uiNotificationAuthority + "/uirecentfolders"); 985 986 987 // All accounts 988 sURIMatcher.addURI(EmailContent.AUTHORITY, "account", ACCOUNT); 989 // A specific account 990 // insert into this URI causes a mailbox to be added to the account 991 sURIMatcher.addURI(EmailContent.AUTHORITY, "account/#", ACCOUNT_ID); 992 sURIMatcher.addURI(EmailContent.AUTHORITY, "accountCheck/#", ACCOUNT_CHECK); 993 994 // Special URI to reset the new message count. Only update works, and values 995 // will be ignored. 996 sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount", 997 ACCOUNT_RESET_NEW_COUNT); 998 sURIMatcher.addURI(EmailContent.AUTHORITY, "resetNewMessageCount/#", 999 ACCOUNT_RESET_NEW_COUNT_ID); 1000 1001 // All mailboxes 1002 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox", MAILBOX); 1003 // A specific mailbox 1004 // insert into this URI causes a message to be added to the mailbox 1005 // ** NOTE For now, the accountKey must be set manually in the values! 1006 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailbox/*", MAILBOX_ID); 1007 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxNotification/#", 1008 MAILBOX_NOTIFICATION); 1009 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxMostRecentMessage/#", 1010 MAILBOX_MOST_RECENT_MESSAGE); 1011 sURIMatcher.addURI(EmailContent.AUTHORITY, "mailboxCount/#", MAILBOX_MESSAGE_COUNT); 1012 1013 // All messages 1014 sURIMatcher.addURI(EmailContent.AUTHORITY, "message", MESSAGE); 1015 // A specific message 1016 // insert into this URI causes an attachment to be added to the message 1017 sURIMatcher.addURI(EmailContent.AUTHORITY, "message/#", MESSAGE_ID); 1018 1019 // A specific attachment 1020 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment", ATTACHMENT); 1021 // A specific attachment (the header information) 1022 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/#", ATTACHMENT_ID); 1023 // The attachments of a specific message (query only) (insert & delete TBD) 1024 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/message/#", 1025 ATTACHMENTS_MESSAGE_ID); 1026 sURIMatcher.addURI(EmailContent.AUTHORITY, "attachment/cachedFile", 1027 ATTACHMENTS_CACHED_FILE_ACCESS); 1028 1029 // All mail bodies 1030 sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY); 1031 // A specific mail body 1032 sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID); 1033 1034 // All hostauth records 1035 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH); 1036 // A specific hostauth 1037 sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); 1038 1039 /** 1040 * THIS URI HAS SPECIAL SEMANTICS 1041 * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK 1042 * TO A SERVER VIA A SYNC ADAPTER 1043 */ 1044 sURIMatcher.addURI(EmailContent.AUTHORITY, "syncedMessage/#", SYNCED_MESSAGE_ID); 1045 sURIMatcher.addURI(EmailContent.AUTHORITY, "messageBySelection", MESSAGE_SELECTION); 1046 1047 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageMove.PATH, MESSAGE_MOVE); 1048 sURIMatcher.addURI(EmailContent.AUTHORITY, MessageStateChange.PATH, 1049 MESSAGE_STATE_CHANGE); 1050 1051 /** 1052 * THE URIs BELOW THIS POINT ARE INTENDED TO BE USED BY SYNC ADAPTERS ONLY 1053 * THEY REFER TO DATA CREATED AND MAINTAINED BY CALLS TO THE SYNCED_MESSAGE_ID URI 1054 * BY THE UI APPLICATION 1055 */ 1056 // All deleted messages 1057 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage", DELETED_MESSAGE); 1058 // A specific deleted message 1059 sURIMatcher.addURI(EmailContent.AUTHORITY, "deletedMessage/#", DELETED_MESSAGE_ID); 1060 1061 // All updated messages 1062 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage", UPDATED_MESSAGE); 1063 // A specific updated message 1064 sURIMatcher.addURI(EmailContent.AUTHORITY, "updatedMessage/#", UPDATED_MESSAGE_ID); 1065 1066 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); 1067 CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); 1068 1069 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); 1070 sURIMatcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); 1071 1072 // All quick responses 1073 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse", QUICK_RESPONSE); 1074 // A specific quick response 1075 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/#", QUICK_RESPONSE_ID); 1076 // All quick responses associated with a particular account id 1077 sURIMatcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", 1078 QUICK_RESPONSE_ACCOUNT_ID); 1079 1080 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolders/#", UI_FOLDERS); 1081 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifullfolders/#", UI_FULL_FOLDERS); 1082 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiallfolders/#", UI_ALL_FOLDERS); 1083 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisubfolders/#", UI_SUBFOLDERS); 1084 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); 1085 sURIMatcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); 1086 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiundo", UI_UNDO); 1087 sURIMatcher.addURI(EmailContent.AUTHORITY, QUERY_UIREFRESH + "/#", UI_FOLDER_REFRESH); 1088 // We listen to everything trailing uifolder/ since there might be an appVersion 1089 // as in Utils.appendVersionQueryParameter(). 1090 sURIMatcher.addURI(EmailContent.AUTHORITY, "uifolder/*", UI_FOLDER); 1091 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccount/#", UI_ACCOUNT); 1092 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccts", UI_ACCTS); 1093 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachments/#", UI_ATTACHMENTS); 1094 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiattachment/#", UI_ATTACHMENT); 1095 sURIMatcher.addURI(EmailContent.AUTHORITY, "uisearch/#", UI_SEARCH); 1096 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiaccountdata/#", UI_ACCOUNT_DATA); 1097 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiloadmore/#", UI_FOLDER_LOAD_MORE); 1098 sURIMatcher.addURI(EmailContent.AUTHORITY, "uiconversation/#", UI_CONVERSATION); 1099 sURIMatcher.addURI(EmailContent.AUTHORITY, "uirecentfolders/#", UI_RECENT_FOLDERS); 1100 sURIMatcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#", 1101 UI_DEFAULT_RECENT_FOLDERS); 1102 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", 1103 ACCOUNT_PICK_TRASH_FOLDER); 1104 sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", 1105 ACCOUNT_PICK_SENT_FOLDER); 1106 } 1107 } 1108 1109 /** 1110 * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must 1111 * always be in sync (i.e. there are two database or NO databases). This code will delete 1112 * any "orphan" database, so that both will be created together. Note that an "orphan" database 1113 * will exist after either of the individual databases is deleted due to data corruption. 1114 */ 1115 public void checkDatabases() { 1116 synchronized (sDatabaseLock) { 1117 // Uncache the databases 1118 if (mDatabase != null) { 1119 mDatabase = null; 1120 } 1121 if (mBodyDatabase != null) { 1122 mBodyDatabase = null; 1123 } 1124 // Look for orphans, and delete as necessary; these must always be in sync 1125 final File databaseFile = getContext().getDatabasePath(DATABASE_NAME); 1126 final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); 1127 1128 // TODO Make sure attachments are deleted 1129 if (databaseFile.exists() && !bodyFile.exists()) { 1130 LogUtils.w(TAG, "Deleting orphaned EmailProvider database..."); 1131 getContext().deleteDatabase(DATABASE_NAME); 1132 } else if (bodyFile.exists() && !databaseFile.exists()) { 1133 LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database..."); 1134 getContext().deleteDatabase(BODY_DATABASE_NAME); 1135 } 1136 } 1137 } 1138 1139 @Override 1140 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1141 String sortOrder) { 1142 Cursor c = null; 1143 int match; 1144 try { 1145 match = findMatch(uri, "query"); 1146 } catch (IllegalArgumentException e) { 1147 String uriString = uri.toString(); 1148 // If we were passed an illegal uri, see if it ends in /-1 1149 // if so, and if substituting 0 for -1 results in a valid uri, return an empty cursor 1150 if (uriString != null && uriString.endsWith("/-1")) { 1151 uri = Uri.parse(uriString.substring(0, uriString.length() - 2) + "0"); 1152 match = findMatch(uri, "query"); 1153 switch (match) { 1154 case BODY_ID: 1155 case MESSAGE_ID: 1156 case DELETED_MESSAGE_ID: 1157 case UPDATED_MESSAGE_ID: 1158 case ATTACHMENT_ID: 1159 case MAILBOX_ID: 1160 case ACCOUNT_ID: 1161 case HOSTAUTH_ID: 1162 case POLICY_ID: 1163 return new MatrixCursorWithCachedColumns(projection, 0); 1164 } 1165 } 1166 throw e; 1167 } 1168 Context context = getContext(); 1169 // See the comment at delete(), above 1170 SQLiteDatabase db = getDatabase(context); 1171 int table = match >> BASE_SHIFT; 1172 String limit = uri.getQueryParameter(EmailContent.PARAMETER_LIMIT); 1173 String id; 1174 1175 String tableName = TABLE_NAMES.valueAt(table); 1176 1177 try { 1178 switch (match) { 1179 // First, dispatch queries from UnfiedEmail 1180 case UI_SEARCH: 1181 c = uiSearch(uri, projection); 1182 return c; 1183 case UI_ACCTS: 1184 c = uiAccounts(projection); 1185 return c; 1186 case UI_UNDO: 1187 return uiUndo(projection); 1188 case UI_SUBFOLDERS: 1189 case UI_MESSAGES: 1190 case UI_MESSAGE: 1191 case UI_FOLDER: 1192 case UI_ACCOUNT: 1193 case UI_ATTACHMENT: 1194 case UI_ATTACHMENTS: 1195 case UI_CONVERSATION: 1196 case UI_RECENT_FOLDERS: 1197 case UI_FULL_FOLDERS: 1198 case UI_ALL_FOLDERS: 1199 // For now, we don't allow selection criteria within these queries 1200 if (selection != null || selectionArgs != null) { 1201 throw new IllegalArgumentException("UI queries can't have selection/args"); 1202 } 1203 1204 final String seenParam = uri.getQueryParameter(UIProvider.SEEN_QUERY_PARAMETER); 1205 final boolean unseenOnly = 1206 seenParam != null && Boolean.FALSE.toString().equals(seenParam); 1207 1208 c = uiQuery(match, uri, projection, unseenOnly); 1209 return c; 1210 case UI_FOLDERS: 1211 c = uiFolders(uri, projection); 1212 return c; 1213 case UI_FOLDER_LOAD_MORE: 1214 c = uiFolderLoadMore(getMailbox(uri)); 1215 return c; 1216 case UI_FOLDER_REFRESH: 1217 c = uiFolderRefresh(getMailbox(uri), 0); 1218 return c; 1219 case MAILBOX_NOTIFICATION: 1220 c = notificationQuery(uri); 1221 return c; 1222 case MAILBOX_MOST_RECENT_MESSAGE: 1223 c = mostRecentMessageQuery(uri); 1224 return c; 1225 case MAILBOX_MESSAGE_COUNT: 1226 c = getMailboxMessageCount(uri); 1227 return c; 1228 case MESSAGE_MOVE: 1229 return db.query(MessageMove.TABLE_NAME, projection, selection, selectionArgs, 1230 null, null, sortOrder, limit); 1231 case MESSAGE_STATE_CHANGE: 1232 return db.query(MessageStateChange.TABLE_NAME, projection, selection, 1233 selectionArgs, null, null, sortOrder, limit); 1234 case BODY: 1235 case MESSAGE: 1236 case UPDATED_MESSAGE: 1237 case DELETED_MESSAGE: 1238 case ATTACHMENT: 1239 case MAILBOX: 1240 case ACCOUNT: 1241 case HOSTAUTH: 1242 case POLICY: 1243 c = db.query(tableName, projection, 1244 selection, selectionArgs, null, null, sortOrder, limit); 1245 break; 1246 case QUICK_RESPONSE: 1247 c = uiQuickResponse(projection); 1248 break; 1249 case BODY_ID: 1250 case MESSAGE_ID: 1251 case DELETED_MESSAGE_ID: 1252 case UPDATED_MESSAGE_ID: 1253 case ATTACHMENT_ID: 1254 case MAILBOX_ID: 1255 case ACCOUNT_ID: 1256 case HOSTAUTH_ID: 1257 case POLICY_ID: 1258 id = uri.getPathSegments().get(1); 1259 c = db.query(tableName, projection, whereWithId(id, selection), 1260 selectionArgs, null, null, sortOrder, limit); 1261 break; 1262 case QUICK_RESPONSE_ID: 1263 id = uri.getPathSegments().get(1); 1264 c = uiQuickResponseId(projection, id); 1265 break; 1266 case ATTACHMENTS_MESSAGE_ID: 1267 // All attachments for the given message 1268 id = uri.getPathSegments().get(2); 1269 c = db.query(Attachment.TABLE_NAME, projection, 1270 whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), 1271 selectionArgs, null, null, sortOrder, limit); 1272 break; 1273 case QUICK_RESPONSE_ACCOUNT_ID: 1274 // All quick responses for the given account 1275 id = uri.getPathSegments().get(2); 1276 c = uiQuickResponseAccount(projection, id); 1277 break; 1278 default: 1279 throw new IllegalArgumentException("Unknown URI " + uri); 1280 } 1281 } catch (SQLiteException e) { 1282 checkDatabases(); 1283 throw e; 1284 } catch (RuntimeException e) { 1285 checkDatabases(); 1286 e.printStackTrace(); 1287 throw e; 1288 } finally { 1289 if (c == null) { 1290 // This should never happen, but let's be sure to log it... 1291 // TODO: There are actually cases where c == null is expected, for example 1292 // UI_FOLDER_LOAD_MORE. 1293 // Demoting this to a warning for now until we figure out what to do with it. 1294 LogUtils.w(TAG, "Query returning null for uri: " + uri + ", selection: " 1295 + selection); 1296 } 1297 } 1298 1299 if ((c != null) && !isTemporary()) { 1300 c.setNotificationUri(getContext().getContentResolver(), uri); 1301 } 1302 return c; 1303 } 1304 1305 private static String whereWithId(String id, String selection) { 1306 StringBuilder sb = new StringBuilder(256); 1307 sb.append("_id="); 1308 sb.append(id); 1309 if (selection != null) { 1310 sb.append(" AND ("); 1311 sb.append(selection); 1312 sb.append(')'); 1313 } 1314 return sb.toString(); 1315 } 1316 1317 /** 1318 * Combine a locally-generated selection with a user-provided selection 1319 * 1320 * This introduces risk that the local selection might insert incorrect chars 1321 * into the SQL, so use caution. 1322 * 1323 * @param where locally-generated selection, must not be null 1324 * @param selection user-provided selection, may be null 1325 * @return a single selection string 1326 */ 1327 private static String whereWith(String where, String selection) { 1328 if (selection == null) { 1329 return where; 1330 } 1331 StringBuilder sb = new StringBuilder(where); 1332 sb.append(" AND ("); 1333 sb.append(selection); 1334 sb.append(')'); 1335 1336 return sb.toString(); 1337 } 1338 1339 /** 1340 * Restore a HostAuth from a database, given its unique id 1341 * @param db the database 1342 * @param id the unique id (_id) of the row 1343 * @return a fully populated HostAuth or null if the row does not exist 1344 */ 1345 private static HostAuth restoreHostAuth(SQLiteDatabase db, long id) { 1346 Cursor c = db.query(HostAuth.TABLE_NAME, HostAuth.CONTENT_PROJECTION, 1347 HostAuth.RECORD_ID + "=?", new String[] {Long.toString(id)}, null, null, null); 1348 try { 1349 if (c.moveToFirst()) { 1350 HostAuth hostAuth = new HostAuth(); 1351 hostAuth.restore(c); 1352 return hostAuth; 1353 } 1354 return null; 1355 } finally { 1356 c.close(); 1357 } 1358 } 1359 1360 /** 1361 * Copy the Account and HostAuth tables from one database to another 1362 * @param fromDatabase the source database 1363 * @param toDatabase the destination database 1364 * @return the number of accounts copied, or -1 if an error occurred 1365 */ 1366 private static int copyAccountTables(SQLiteDatabase fromDatabase, SQLiteDatabase toDatabase) { 1367 if (fromDatabase == null || toDatabase == null) return -1; 1368 1369 // Lock both databases; for the "from" database, we don't want anyone changing it from 1370 // under us; for the "to" database, we want to make the operation atomic 1371 int copyCount = 0; 1372 fromDatabase.beginTransaction(); 1373 try { 1374 toDatabase.beginTransaction(); 1375 try { 1376 // Delete anything hanging around here 1377 toDatabase.delete(Account.TABLE_NAME, null, null); 1378 toDatabase.delete(HostAuth.TABLE_NAME, null, null); 1379 1380 // Get our account cursor 1381 Cursor c = fromDatabase.query(Account.TABLE_NAME, Account.CONTENT_PROJECTION, 1382 null, null, null, null, null); 1383 if (c == null) return 0; 1384 LogUtils.d(TAG, "fromDatabase accounts: " + c.getCount()); 1385 try { 1386 // Loop through accounts, copying them and associated host auth's 1387 while (c.moveToNext()) { 1388 Account account = new Account(); 1389 account.restore(c); 1390 1391 // Clear security sync key and sync key, as these were specific to the 1392 // state of the account, and we've reset that... 1393 // Clear policy key so that we can re-establish policies from the server 1394 // TODO This is pretty EAS specific, but there's a lot of that around 1395 account.mSecuritySyncKey = null; 1396 account.mSyncKey = null; 1397 account.mPolicyKey = 0; 1398 1399 // Copy host auth's and update foreign keys 1400 HostAuth hostAuth = restoreHostAuth(fromDatabase, 1401 account.mHostAuthKeyRecv); 1402 1403 // The account might have gone away, though very unlikely 1404 if (hostAuth == null) continue; 1405 account.mHostAuthKeyRecv = toDatabase.insert(HostAuth.TABLE_NAME, null, 1406 hostAuth.toContentValues()); 1407 1408 // EAS accounts have no send HostAuth 1409 if (account.mHostAuthKeySend > 0) { 1410 hostAuth = restoreHostAuth(fromDatabase, account.mHostAuthKeySend); 1411 // Belt and suspenders; I can't imagine that this is possible, 1412 // since we checked the validity of the account above, and the 1413 // database is now locked 1414 if (hostAuth == null) continue; 1415 account.mHostAuthKeySend = toDatabase.insert( 1416 HostAuth.TABLE_NAME, null, hostAuth.toContentValues()); 1417 } 1418 1419 // Now, create the account in the "to" database 1420 toDatabase.insert(Account.TABLE_NAME, null, account.toContentValues()); 1421 copyCount++; 1422 } 1423 } finally { 1424 c.close(); 1425 } 1426 1427 // Say it's ok to commit 1428 toDatabase.setTransactionSuccessful(); 1429 } finally { 1430 toDatabase.endTransaction(); 1431 } 1432 } catch (SQLiteException ex) { 1433 LogUtils.w(TAG, "Exception while copying account tables", ex); 1434 copyCount = -1; 1435 } finally { 1436 fromDatabase.endTransaction(); 1437 } 1438 return copyCount; 1439 } 1440 1441 private static SQLiteDatabase getBackupDatabase(Context context) { 1442 DBHelper.DatabaseHelper helper = new DBHelper.DatabaseHelper(context, BACKUP_DATABASE_NAME); 1443 return helper.getWritableDatabase(); 1444 } 1445 1446 /** 1447 * Backup account data, returning the number of accounts backed up 1448 */ 1449 private static int backupAccounts(Context context, SQLiteDatabase mainDatabase) { 1450 if (MailActivityEmail.DEBUG) { 1451 LogUtils.d(TAG, "backupAccounts..."); 1452 } 1453 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1454 try { 1455 int numBackedUp = copyAccountTables(mainDatabase, backupDatabase); 1456 if (numBackedUp < 0) { 1457 LogUtils.e(TAG, "Account backup failed!"); 1458 } else if (MailActivityEmail.DEBUG) { 1459 LogUtils.d(TAG, "Backed up " + numBackedUp + " accounts..."); 1460 } 1461 return numBackedUp; 1462 } finally { 1463 if (backupDatabase != null) { 1464 backupDatabase.close(); 1465 } 1466 } 1467 } 1468 1469 /** 1470 * Restore account data, returning the number of accounts restored 1471 */ 1472 private static int restoreAccounts(Context context, SQLiteDatabase mainDatabase) { 1473 if (MailActivityEmail.DEBUG) { 1474 LogUtils.d(TAG, "restoreAccounts..."); 1475 } 1476 SQLiteDatabase backupDatabase = getBackupDatabase(context); 1477 try { 1478 int numRecovered = copyAccountTables(backupDatabase, mainDatabase); 1479 if (numRecovered > 0) { 1480 LogUtils.e(TAG, "Recovered " + numRecovered + " accounts!"); 1481 } else if (numRecovered < 0) { 1482 LogUtils.e(TAG, "Account recovery failed?"); 1483 } else if (MailActivityEmail.DEBUG) { 1484 LogUtils.d(TAG, "No accounts to restore..."); 1485 } 1486 return numRecovered; 1487 } finally { 1488 if (backupDatabase != null) { 1489 backupDatabase.close(); 1490 } 1491 } 1492 } 1493 1494 private static final String MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX = "insert into %s (" 1495 + MessageChangeLogTable.MESSAGE_KEY + "," + MessageChangeLogTable.SERVER_ID + "," 1496 + MessageChangeLogTable.ACCOUNT_KEY + "," + MessageChangeLogTable.STATUS + ","; 1497 1498 private static final String MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX = ") values (%s, " 1499 + "(select " + Message.SERVER_ID + " from " + Message.TABLE_NAME + " where _id=%s)," 1500 + "(select " + Message.ACCOUNT_KEY + " from " + Message.TABLE_NAME + " where _id=%s)," 1501 + MessageMove.STATUS_NONE_STRING + ","; 1502 1503 /** 1504 * Formatting string to generate the SQL statement for inserting into MessageMove. 1505 * The formatting parameters are: 1506 * table name, message id x 4, destination folder id, message id, destination folder id. 1507 * Duplications are needed for sub-selects. 1508 */ 1509 private static final String MESSAGE_MOVE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1510 + MessageMove.SRC_FOLDER_KEY + "," + MessageMove.DST_FOLDER_KEY + "," 1511 + MessageMove.SRC_FOLDER_SERVER_ID + "," + MessageMove.DST_FOLDER_SERVER_ID 1512 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1513 + "(select " + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s)," 1514 + "%d," 1515 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=(select " 1516 + Message.MAILBOX_KEY + " from " + Message.TABLE_NAME + " where _id=%s))," 1517 + "(select " + Mailbox.SERVER_ID + " from " + Mailbox.TABLE_NAME + " where _id=%d))"; 1518 1519 /** 1520 * Insert a row into the MessageMove table when that message is moved. 1521 * @param db The {@link SQLiteDatabase}. 1522 * @param messageId The id of the message being moved. 1523 * @param dstFolderKey The folder to which the message is being moved. 1524 */ 1525 private void addToMessageMove(final SQLiteDatabase db, final String messageId, 1526 final long dstFolderKey) { 1527 db.execSQL(String.format(Locale.US, MESSAGE_MOVE_INSERT, MessageMove.TABLE_NAME, 1528 messageId, messageId, messageId, messageId, dstFolderKey, messageId, dstFolderKey)); 1529 } 1530 1531 /** 1532 * Formatting string to generate the SQL statement for inserting into MessageStateChange. 1533 * The formatting parameters are: 1534 * table name, message id x 4, new flag read, message id, new flag favorite. 1535 * Duplications are needed for sub-selects. 1536 */ 1537 private static final String MESSAGE_STATE_CHANGE_INSERT = MESSAGE_CHANGE_LOG_TABLE_INSERT_PREFIX 1538 + MessageStateChange.OLD_FLAG_READ + "," + MessageStateChange.NEW_FLAG_READ + "," 1539 + MessageStateChange.OLD_FLAG_FAVORITE + "," + MessageStateChange.NEW_FLAG_FAVORITE 1540 + MESSAGE_CHANGE_LOG_TABLE_VALUES_PREFIX 1541 + "(select " + Message.FLAG_READ + " from " + Message.TABLE_NAME + " where _id=%s)," 1542 + "%d," 1543 + "(select " + Message.FLAG_FAVORITE + " from " + Message.TABLE_NAME + " where _id=%s)," 1544 + "%d)"; 1545 1546 private void addToMessageStateChange(final SQLiteDatabase db, final String messageId, 1547 final int newFlagRead, final int newFlagFavorite) { 1548 db.execSQL(String.format(Locale.US, MESSAGE_STATE_CHANGE_INSERT, 1549 MessageStateChange.TABLE_NAME, messageId, messageId, messageId, messageId, 1550 newFlagRead, messageId, newFlagFavorite)); 1551 } 1552 1553 // select count(*) from (select count(*) as dupes from Mailbox where accountKey=? 1554 // group by serverId) where dupes > 1; 1555 private static final String ACCOUNT_INTEGRITY_SQL = 1556 "select count(*) from (select count(*) as dupes from " + Mailbox.TABLE_NAME + 1557 " where accountKey=? group by " + MailboxColumns.SERVER_ID + ") where dupes > 1"; 1558 1559 1560 // Query to get the protocol for a message. Temporary to switch between new and old upsync 1561 // behavior; should go away when IMAP gets converted. 1562 private static final String GET_MESSAGE_DETAILS = "SELECT" 1563 + " h." + HostAuthColumns.PROTOCOL + "," 1564 + " m." + Message.MAILBOX_KEY + "," 1565 + " a." + AccountColumns.ID 1566 + " FROM " + Message.TABLE_NAME + " AS m" 1567 + " INNER JOIN " + Account.TABLE_NAME + " AS a" 1568 + " ON m." + MessageColumns.ACCOUNT_KEY + "=a." + AccountColumns.ID 1569 + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" 1570 + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns.ID 1571 + " WHERE m." + MessageColumns.ID + "=?"; 1572 private static final int INDEX_PROTOCOL = 0; 1573 private static final int INDEX_MAILBOX_KEY = 1; 1574 private static final int INDEX_ACCOUNT_KEY = 2; 1575 1576 /** 1577 * Query to get the protocol and email address for an account. Note that this uses 1578 * {@link #INDEX_PROTOCOL} and {@link #INDEX_EMAIL_ADDRESS} for its columns. 1579 */ 1580 private static final String GET_ACCOUNT_DETAILS = "SELECT" 1581 + " h." + HostAuthColumns.PROTOCOL + "," 1582 + " a." + AccountColumns.EMAIL_ADDRESS + "," 1583 + " a." + AccountColumns.SYNC_KEY 1584 + " FROM " + Account.TABLE_NAME + " AS a" 1585 + " INNER JOIN " + HostAuth.TABLE_NAME + " AS h" 1586 + " ON a." + AccountColumns.HOST_AUTH_KEY_RECV + "=h." + HostAuthColumns.ID 1587 + " WHERE a." + AccountColumns.ID + "=?"; 1588 private static final int INDEX_EMAIL_ADDRESS = 1; 1589 private static final int INDEX_SYNC_KEY = 2; 1590 1591 /** 1592 * Restart push if we need it (currently only for Exchange accounts). 1593 * @param context A {@link Context}. 1594 * @param db The {@link SQLiteDatabase}. 1595 * @param id The id of the thing we're looking for. 1596 * @return Whether or not we sent a request to restart the push. 1597 */ 1598 private static boolean restartPush(final Context context, final SQLiteDatabase db, 1599 final String id) { 1600 final Cursor c = db.rawQuery(GET_ACCOUNT_DETAILS, new String[] {id}); 1601 if (c != null) { 1602 try { 1603 if (c.moveToFirst()) { 1604 final String protocol = c.getString(INDEX_PROTOCOL); 1605 // Only restart push for EAS accounts that have completed initial sync. 1606 if (context.getString(R.string.protocol_eas).equals(protocol) && 1607 !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) { 1608 final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS); 1609 final android.accounts.Account account = 1610 getAccountManagerAccount(context, emailAddress, protocol); 1611 restartPush(account); 1612 return true; 1613 } 1614 } 1615 } finally { 1616 c.close(); 1617 } 1618 } 1619 return false; 1620 } 1621 1622 /** 1623 * Restart push if a mailbox's settings change in a way that requires it. 1624 * @param context A {@link Context}. 1625 * @param db The {@link SQLiteDatabase}. 1626 * @param values The {@link ContentValues} that were updated for the mailbox. 1627 * @param accountId The id of the account for this mailbox. 1628 * @return Whether or not the push was restarted. 1629 */ 1630 private static boolean restartPushForMailbox(final Context context, final SQLiteDatabase db, 1631 final ContentValues values, final String accountId) { 1632 if (values.containsKey(MailboxColumns.SYNC_LOOKBACK) || 1633 values.containsKey(MailboxColumns.SYNC_INTERVAL)) { 1634 return restartPush(context, db, accountId); 1635 } 1636 return false; 1637 } 1638 1639 /** 1640 * Restart push if an account's settings change in a way that requires it. 1641 * @param context A {@link Context}. 1642 * @param db The {@link SQLiteDatabase}. 1643 * @param values The {@link ContentValues} that were updated for the account. 1644 * @param accountId The id of the account. 1645 * @return Whether or not the push was restarted. 1646 */ 1647 private static boolean restartPushForAccount(final Context context, final SQLiteDatabase db, 1648 final ContentValues values, final String accountId) { 1649 if (values.containsKey(AccountColumns.SYNC_LOOKBACK) || 1650 values.containsKey(AccountColumns.SYNC_INTERVAL)) { 1651 return restartPush(context, db, accountId); 1652 } 1653 return false; 1654 } 1655 1656 @Override 1657 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1658 LogUtils.d(TAG, "Update: " + uri); 1659 // Handle this special case the fastest possible way 1660 if (uri == INTEGRITY_CHECK_URI) { 1661 checkDatabases(); 1662 return 0; 1663 } else if (uri == ACCOUNT_BACKUP_URI) { 1664 return backupAccounts(getContext(), getDatabase(getContext())); 1665 } 1666 1667 // Notify all existing cursors, except for ACCOUNT_RESET_NEW_COUNT(_ID) 1668 Uri notificationUri = EmailContent.CONTENT_URI; 1669 1670 int match = findMatch(uri, "update"); 1671 Context context = getContext(); 1672 ContentResolver resolver = context.getContentResolver(); 1673 // See the comment at delete(), above 1674 SQLiteDatabase db = getDatabase(context); 1675 int table = match >> BASE_SHIFT; 1676 int result; 1677 1678 // We do NOT allow setting of unreadCount/messageCount via the provider 1679 // These columns are maintained via triggers 1680 if (match == MAILBOX_ID || match == MAILBOX) { 1681 values.remove(MailboxColumns.UNREAD_COUNT); 1682 values.remove(MailboxColumns.MESSAGE_COUNT); 1683 } 1684 1685 String tableName = TABLE_NAMES.valueAt(table); 1686 String id = "0"; 1687 1688 try { 1689 switch (match) { 1690 case ACCOUNT_PICK_TRASH_FOLDER: 1691 return pickTrashFolder(uri); 1692 case ACCOUNT_PICK_SENT_FOLDER: 1693 return pickSentFolder(uri); 1694 case UI_FOLDER: 1695 return uiUpdateFolder(context, uri, values); 1696 case UI_RECENT_FOLDERS: 1697 return uiUpdateRecentFolders(uri, values); 1698 case UI_DEFAULT_RECENT_FOLDERS: 1699 return uiPopulateRecentFolders(uri); 1700 case UI_ATTACHMENT: 1701 return uiUpdateAttachment(uri, values); 1702 case UI_MESSAGE: 1703 return uiUpdateMessage(uri, values); 1704 case ACCOUNT_CHECK: 1705 id = uri.getLastPathSegment(); 1706 // With any error, return 1 (a failure) 1707 int res = 1; 1708 Cursor ic = null; 1709 try { 1710 ic = db.rawQuery(ACCOUNT_INTEGRITY_SQL, new String[] {id}); 1711 if (ic.moveToFirst()) { 1712 res = ic.getInt(0); 1713 } 1714 } finally { 1715 if (ic != null) { 1716 ic.close(); 1717 } 1718 } 1719 // Count of duplicated mailboxes 1720 return res; 1721 case MESSAGE_SELECTION: 1722 Cursor findCursor = db.query(tableName, Message.ID_COLUMN_PROJECTION, selection, 1723 selectionArgs, null, null, null); 1724 try { 1725 if (findCursor.moveToFirst()) { 1726 return update(ContentUris.withAppendedId( 1727 Message.CONTENT_URI, 1728 findCursor.getLong(Message.ID_COLUMNS_ID_COLUMN)), 1729 values, null, null); 1730 } else { 1731 return 0; 1732 } 1733 } finally { 1734 findCursor.close(); 1735 } 1736 case SYNCED_MESSAGE_ID: 1737 case UPDATED_MESSAGE_ID: 1738 case MESSAGE_ID: 1739 case BODY_ID: 1740 case ATTACHMENT_ID: 1741 case MAILBOX_ID: 1742 case ACCOUNT_ID: 1743 case HOSTAUTH_ID: 1744 case QUICK_RESPONSE_ID: 1745 case POLICY_ID: 1746 id = uri.getPathSegments().get(1); 1747 if (match == SYNCED_MESSAGE_ID) { 1748 // TODO: Migrate IMAP to use MessageMove/MessageStateChange as well. 1749 boolean isEas = false; 1750 long mailboxId = -1; 1751 long accountId = -1; 1752 final Cursor c = db.rawQuery(GET_MESSAGE_DETAILS, new String[] {id}); 1753 if (c != null) { 1754 try { 1755 if (c.moveToFirst()) { 1756 final String protocol = c.getString(INDEX_PROTOCOL); 1757 isEas = context.getString(R.string.protocol_eas) 1758 .equals(protocol); 1759 mailboxId = c.getLong(INDEX_MAILBOX_KEY); 1760 accountId = c.getLong(INDEX_ACCOUNT_KEY); 1761 } 1762 } finally { 1763 c.close(); 1764 } 1765 } 1766 1767 if (isEas) { 1768 // EAS uses the new upsync classes. 1769 Long dstFolderId = values.getAsLong(MessageColumns.MAILBOX_KEY); 1770 if (dstFolderId != null) { 1771 addToMessageMove(db, id, dstFolderId); 1772 } 1773 Integer flagRead = values.getAsInteger(MessageColumns.FLAG_READ); 1774 Integer flagFavorite = values.getAsInteger(MessageColumns.FLAG_FAVORITE); 1775 int flagReadValue = (flagRead != null) ? 1776 flagRead : MessageStateChange.VALUE_UNCHANGED; 1777 int flagFavoriteValue = (flagFavorite != null) ? 1778 flagFavorite : MessageStateChange.VALUE_UNCHANGED; 1779 if (flagRead != null || flagFavorite != null) { 1780 addToMessageStateChange(db, id, flagReadValue, flagFavoriteValue); 1781 } 1782 1783 // Request a sync for the messages mailbox so the update will upsync. 1784 // This is normally done with ContentResolver.notifyUpdate() but doesn't 1785 // work for Exchange because the Sync Adapter is declared as 1786 // android:supportsUploading="false". Changing it to true is not trivial 1787 // because that would require us to protect all calls to notifyUpdate() 1788 // with syncToServer=false except in cases where we actually want to 1789 // upsync. 1790 // TODO: Look into making Exchange Sync Adapter supportsUploading=true 1791 // Since we can't use the Sync Manager "delayed-sync" feature which 1792 // applies only to UPLOAD syncs, we need to do this ourselves. The 1793 // purpose of this is not to spam syncs when making frequent 1794 // modifications. 1795 final Handler handler = getDelayedSyncHandler(); 1796 final android.accounts.Account amAccount = 1797 getAccountManagerAccount(accountId); 1798 if (amAccount != null) { 1799 final SyncRequestMessage request = new SyncRequestMessage( 1800 uri.getAuthority(), amAccount, mailboxId); 1801 synchronized (mDelayedSyncRequests) { 1802 if (!mDelayedSyncRequests.contains(request)) { 1803 mDelayedSyncRequests.add(request); 1804 final android.os.Message message = 1805 handler.obtainMessage(0, request); 1806 handler.sendMessageDelayed(message, SYNC_DELAY_MILLIS); 1807 } 1808 } 1809 } else { 1810 LogUtils.d(TAG, 1811 "Attempted to start delayed sync for invalid account %d", 1812 accountId); 1813 } 1814 } else { 1815 // Old way of doing upsync. 1816 // For synced messages, first copy the old message to the updated table 1817 // Note the insert or ignore semantics, guaranteeing that only the first 1818 // update will be reflected in the updated message table; therefore this 1819 // row will always have the "original" data 1820 db.execSQL(UPDATED_MESSAGE_INSERT + id); 1821 } 1822 } else if (match == MESSAGE_ID) { 1823 db.execSQL(UPDATED_MESSAGE_DELETE + id); 1824 } 1825 result = db.update(tableName, values, whereWithId(id, selection), 1826 selectionArgs); 1827 if (match == MESSAGE_ID || match == SYNCED_MESSAGE_ID) { 1828 handleMessageUpdateNotifications(uri, id, values); 1829 } else if (match == ATTACHMENT_ID) { 1830 long attId = Integer.parseInt(id); 1831 if (values.containsKey(Attachment.FLAGS)) { 1832 int flags = values.getAsInteger(Attachment.FLAGS); 1833 mAttachmentService.attachmentChanged(context, attId, flags); 1834 } 1835 // Notify UI if necessary; there are only two columns we can change that 1836 // would be worth a notification 1837 if (values.containsKey(AttachmentColumns.UI_STATE) || 1838 values.containsKey(AttachmentColumns.UI_DOWNLOADED_SIZE)) { 1839 // Notify on individual attachment 1840 notifyUI(UIPROVIDER_ATTACHMENT_NOTIFIER, id); 1841 Attachment att = Attachment.restoreAttachmentWithId(context, attId); 1842 if (att != null) { 1843 // And on owning Message 1844 notifyUI(UIPROVIDER_ATTACHMENTS_NOTIFIER, att.mMessageKey); 1845 } 1846 } 1847 } else if (match == MAILBOX_ID) { 1848 final long accountId = Mailbox.getAccountIdForMailbox(context, id); 1849 notifyUIFolder(id, accountId); 1850 restartPushForMailbox(context, db, values, Long.toString(accountId)); 1851 } else if (match == ACCOUNT_ID) { 1852 updateAccountSyncInterval(Long.parseLong(id), values); 1853 // Notify individual account and "all accounts" 1854 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); 1855 resolver.notifyChange(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 1856 restartPushForAccount(context, db, values, id); 1857 } 1858 break; 1859 case BODY: 1860 case MESSAGE: 1861 case UPDATED_MESSAGE: 1862 case ATTACHMENT: 1863 case MAILBOX: 1864 case ACCOUNT: 1865 case HOSTAUTH: 1866 case POLICY: 1867 result = db.update(tableName, values, selection, selectionArgs); 1868 break; 1869 1870 case ACCOUNT_RESET_NEW_COUNT_ID: 1871 id = uri.getPathSegments().get(1); 1872 ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; 1873 if (values != null) { 1874 Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); 1875 if (set != null) { 1876 newMessageCount = new ContentValues(); 1877 newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); 1878 } 1879 } 1880 result = db.update(tableName, newMessageCount, 1881 whereWithId(id, selection), selectionArgs); 1882 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1883 break; 1884 case ACCOUNT_RESET_NEW_COUNT: 1885 result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, 1886 selection, selectionArgs); 1887 // Affects all accounts. Just invalidate all account cache. 1888 notificationUri = Account.CONTENT_URI; // Only notify account cursors. 1889 break; 1890 case MESSAGE_MOVE: 1891 result = db.update(MessageMove.TABLE_NAME, values, selection, selectionArgs); 1892 break; 1893 case MESSAGE_STATE_CHANGE: 1894 result = db.update(MessageStateChange.TABLE_NAME, values, selection, 1895 selectionArgs); 1896 break; 1897 default: 1898 throw new IllegalArgumentException("Unknown URI " + uri); 1899 } 1900 } catch (SQLiteException e) { 1901 checkDatabases(); 1902 throw e; 1903 } 1904 1905 // Notify all notifier cursors 1906 sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); 1907 1908 resolver.notifyChange(notificationUri, null); 1909 return result; 1910 } 1911 1912 private void updateSyncStatus(final Bundle extras) { 1913 final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); 1914 final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE); 1915 final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id); 1916 EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 1917 final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS; 1918 if (inProgress) { 1919 RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id); 1920 } else { 1921 final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT); 1922 final ContentValues values = new ContentValues(); 1923 values.put(Mailbox.UI_LAST_SYNC_RESULT, result); 1924 mDatabase.update( 1925 Mailbox.TABLE_NAME, 1926 values, 1927 WHERE_ID, 1928 new String[] { String.valueOf(id) }); 1929 } 1930 } 1931 1932 @Override 1933 public Bundle call(String method, String arg, Bundle extras) { 1934 LogUtils.d(TAG, "EmailProvider#call(%s, %s)", method, arg); 1935 1936 // Handle sync status callbacks. 1937 if (TextUtils.equals(method, SYNC_STATUS_CALLBACK_METHOD)) { 1938 updateSyncStatus(extras); 1939 return null; 1940 } 1941 if (TextUtils.equals(method, MailboxUtilities.FIX_PARENT_KEYS_METHOD)) { 1942 fixParentKeys(getDatabase(getContext())); 1943 return null; 1944 } 1945 1946 // Handle send & save. 1947 final Uri accountUri = Uri.parse(arg); 1948 final long accountId = Long.parseLong(accountUri.getPathSegments().get(1)); 1949 1950 Uri messageUri = null; 1951 1952 if (TextUtils.equals(method, UIProvider.AccountCallMethods.SEND_MESSAGE)) { 1953 messageUri = uiSendDraftMessage(accountId, extras); 1954 Preferences.getPreferences(getContext()).setLastUsedAccountId(accountId); 1955 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SAVE_MESSAGE)) { 1956 messageUri = uiSaveDraftMessage(accountId, extras); 1957 } else if (TextUtils.equals(method, UIProvider.AccountCallMethods.SET_CURRENT_ACCOUNT)) { 1958 LogUtils.d(TAG, "Unhandled (but expected) Content provider method: %s", method); 1959 } else { 1960 LogUtils.wtf(TAG, "Unexpected Content provider method: %s", method); 1961 } 1962 1963 final Bundle result; 1964 if (messageUri != null) { 1965 result = new Bundle(1); 1966 result.putParcelable(UIProvider.MessageColumns.URI, messageUri); 1967 } else { 1968 result = null; 1969 } 1970 1971 return result; 1972 } 1973 1974 @Override 1975 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 1976 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) { 1977 LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri)); 1978 } 1979 1980 final int match = findMatch(uri, "openFile"); 1981 switch (match) { 1982 case ATTACHMENTS_CACHED_FILE_ACCESS: 1983 // Parse the cache file path out from the uri 1984 final String cachedFilePath = 1985 uri.getQueryParameter(EmailContent.Attachment.CACHED_FILE_QUERY_PARAM); 1986 1987 if (cachedFilePath != null) { 1988 // clearCallingIdentity means that the download manager will 1989 // check our permissions rather than the permissions of whatever 1990 // code is calling us. 1991 long binderToken = Binder.clearCallingIdentity(); 1992 try { 1993 LogUtils.d(TAG, "Opening attachment %s", cachedFilePath); 1994 return ParcelFileDescriptor.open( 1995 new File(cachedFilePath), ParcelFileDescriptor.MODE_READ_ONLY); 1996 } finally { 1997 Binder.restoreCallingIdentity(binderToken); 1998 } 1999 } 2000 break; 2001 } 2002 2003 throw new FileNotFoundException("unable to open file"); 2004 } 2005 2006 2007 /** 2008 * Returns the base notification URI for the given content type. 2009 * 2010 * @param match The type of content that was modified. 2011 */ 2012 private static Uri getBaseNotificationUri(int match) { 2013 Uri baseUri = null; 2014 switch (match) { 2015 case MESSAGE: 2016 case MESSAGE_ID: 2017 case SYNCED_MESSAGE_ID: 2018 baseUri = Message.NOTIFIER_URI; 2019 break; 2020 case ACCOUNT: 2021 case ACCOUNT_ID: 2022 baseUri = Account.NOTIFIER_URI; 2023 break; 2024 } 2025 return baseUri; 2026 } 2027 2028 /** 2029 * Sends a change notification to any cursors observers of the given base URI. The final 2030 * notification URI is dynamically built to contain the specified information. It will be 2031 * of the format <<baseURI>>/<<op>>/<<id>>; where <<op>> and <<id>> are optional depending 2032 * upon the given values. 2033 * NOTE: If <<op>> is specified, notifications for <<baseURI>>/<<id>> will NOT be invoked. 2034 * If this is necessary, it can be added. However, due to the implementation of 2035 * {@link ContentObserver}, observers of <<baseURI>> will receive multiple notifications. 2036 * 2037 * @param baseUri The base URI to send notifications to. Must be able to take appended IDs. 2038 * @param op Optional operation to be appended to the URI. 2039 * @param id If a positive value, the ID to append to the base URI. Otherwise, no ID will be 2040 * appended to the base URI. 2041 */ 2042 private void sendNotifierChange(Uri baseUri, String op, String id) { 2043 if (baseUri == null) return; 2044 2045 final ContentResolver resolver = getContext().getContentResolver(); 2046 2047 // Append the operation, if specified 2048 if (op != null) { 2049 baseUri = baseUri.buildUpon().appendEncodedPath(op).build(); 2050 } 2051 2052 long longId = 0L; 2053 try { 2054 longId = Long.valueOf(id); 2055 } catch (NumberFormatException ignore) {} 2056 if (longId > 0) { 2057 resolver.notifyChange(ContentUris.withAppendedId(baseUri, longId), null); 2058 } else { 2059 resolver.notifyChange(baseUri, null); 2060 } 2061 2062 // We want to send the message list changed notification if baseUri is Message.NOTIFIER_URI. 2063 if (baseUri.equals(Message.NOTIFIER_URI)) { 2064 sendMessageListDataChangedNotification(); 2065 } 2066 } 2067 2068 private void sendMessageListDataChangedNotification() { 2069 final Context context = getContext(); 2070 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); 2071 // Ideally this intent would contain information about which account changed, to limit the 2072 // updates to that particular account. Unfortunately, that information is not available in 2073 // sendNotifierChange(). 2074 context.sendBroadcast(intent); 2075 } 2076 2077 @Override 2078 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2079 throws OperationApplicationException { 2080 Context context = getContext(); 2081 SQLiteDatabase db = getDatabase(context); 2082 db.beginTransaction(); 2083 try { 2084 ContentProviderResult[] results = super.applyBatch(operations); 2085 db.setTransactionSuccessful(); 2086 return results; 2087 } finally { 2088 db.endTransaction(); 2089 } 2090 } 2091 2092 public static interface AttachmentService { 2093 /** 2094 * Notify the service that an attachment has changed. 2095 */ 2096 void attachmentChanged(Context context, long id, int flags); 2097 } 2098 2099 private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() { 2100 @Override 2101 public void attachmentChanged(Context context, long id, int flags) { 2102 // The default implementation delegates to the real service. 2103 AttachmentDownloadService.attachmentChanged(context, id, flags); 2104 } 2105 }; 2106 private final AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE; 2107 2108 private Cursor notificationQuery(final Uri uri) { 2109 final SQLiteDatabase db = getDatabase(getContext()); 2110 final String accountId = uri.getLastPathSegment(); 2111 2112 final StringBuilder sqlBuilder = new StringBuilder(); 2113 sqlBuilder.append("SELECT "); 2114 sqlBuilder.append(MessageColumns.MAILBOX_KEY).append(", "); 2115 sqlBuilder.append("SUM(CASE ") 2116 .append(MessageColumns.FLAG_READ).append(" WHEN 0 THEN 1 ELSE 0 END), "); 2117 sqlBuilder.append("SUM(CASE ") 2118 .append(MessageColumns.FLAG_SEEN).append(" WHEN 0 THEN 1 ELSE 0 END)\n"); 2119 sqlBuilder.append("FROM "); 2120 sqlBuilder.append(Message.TABLE_NAME).append('\n'); 2121 sqlBuilder.append("WHERE "); 2122 sqlBuilder.append(MessageColumns.ACCOUNT_KEY).append(" = ?\n"); 2123 sqlBuilder.append("GROUP BY "); 2124 sqlBuilder.append(MessageColumns.MAILBOX_KEY); 2125 2126 final String sql = sqlBuilder.toString(); 2127 2128 final String[] selectionArgs = {accountId}; 2129 2130 return db.rawQuery(sql, selectionArgs); 2131 } 2132 2133 public Cursor mostRecentMessageQuery(Uri uri) { 2134 SQLiteDatabase db = getDatabase(getContext()); 2135 String mailboxId = uri.getLastPathSegment(); 2136 return db.rawQuery("select max(_id) from Message where mailboxKey=?", 2137 new String[] {mailboxId}); 2138 } 2139 2140 private Cursor getMailboxMessageCount(Uri uri) { 2141 SQLiteDatabase db = getDatabase(getContext()); 2142 String mailboxId = uri.getLastPathSegment(); 2143 return db.rawQuery("select count(*) from Message where mailboxKey=?", 2144 new String[] {mailboxId}); 2145 } 2146 2147 /** 2148 * Support for UnifiedEmail below 2149 */ 2150 2151 private static final String NOT_A_DRAFT_STRING = 2152 Integer.toString(UIProvider.DraftType.NOT_A_DRAFT); 2153 2154 private static final String CONVERSATION_FLAGS = 2155 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2156 ") !=0 THEN " + UIProvider.ConversationFlags.CALENDAR_INVITE + 2157 " ELSE 0 END + " + 2158 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_FORWARDED + 2159 ") !=0 THEN " + UIProvider.ConversationFlags.FORWARDED + 2160 " ELSE 0 END + " + 2161 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_REPLIED_TO + 2162 ") !=0 THEN " + UIProvider.ConversationFlags.REPLIED + 2163 " ELSE 0 END"; 2164 2165 /** 2166 * Array of pre-defined account colors (legacy colors from old email app) 2167 */ 2168 private static final int[] ACCOUNT_COLORS = new int[] { 2169 0xff71aea7, 0xff621919, 0xff18462f, 0xffbf8e52, 0xff001f79, 2170 0xffa8afc2, 0xff6b64c4, 0xff738359, 0xff9d50a4 2171 }; 2172 2173 private static final String CONVERSATION_COLOR = 2174 "@CASE (" + MessageColumns.ACCOUNT_KEY + " - 1) % " + ACCOUNT_COLORS.length + 2175 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2176 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2177 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2178 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2179 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2180 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2181 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2182 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2183 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2184 " END"; 2185 2186 private static final String ACCOUNT_COLOR = 2187 "@CASE (" + AccountColumns.ID + " - 1) % " + ACCOUNT_COLORS.length + 2188 " WHEN 0 THEN " + ACCOUNT_COLORS[0] + 2189 " WHEN 1 THEN " + ACCOUNT_COLORS[1] + 2190 " WHEN 2 THEN " + ACCOUNT_COLORS[2] + 2191 " WHEN 3 THEN " + ACCOUNT_COLORS[3] + 2192 " WHEN 4 THEN " + ACCOUNT_COLORS[4] + 2193 " WHEN 5 THEN " + ACCOUNT_COLORS[5] + 2194 " WHEN 6 THEN " + ACCOUNT_COLORS[6] + 2195 " WHEN 7 THEN " + ACCOUNT_COLORS[7] + 2196 " WHEN 8 THEN " + ACCOUNT_COLORS[8] + 2197 " END"; 2198 /** 2199 * Mapping of UIProvider columns to EmailProvider columns for the message list (called the 2200 * conversation list in UnifiedEmail) 2201 */ 2202 private static ProjectionMap getMessageListMap() { 2203 if (sMessageListMap == null) { 2204 sMessageListMap = ProjectionMap.builder() 2205 .add(BaseColumns._ID, MessageColumns.ID) 2206 .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) 2207 .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) 2208 .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) 2209 .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) 2210 .add(UIProvider.ConversationColumns.CONVERSATION_INFO, null) 2211 .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) 2212 .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) 2213 .add(UIProvider.ConversationColumns.NUM_MESSAGES, "1") 2214 .add(UIProvider.ConversationColumns.NUM_DRAFTS, "0") 2215 .add(UIProvider.ConversationColumns.SENDING_STATE, 2216 Integer.toString(ConversationSendingState.OTHER)) 2217 .add(UIProvider.ConversationColumns.PRIORITY, 2218 Integer.toString(ConversationPriority.LOW)) 2219 .add(UIProvider.ConversationColumns.READ, MessageColumns.FLAG_READ) 2220 .add(UIProvider.ConversationColumns.SEEN, MessageColumns.FLAG_SEEN) 2221 .add(UIProvider.ConversationColumns.STARRED, MessageColumns.FLAG_FAVORITE) 2222 .add(UIProvider.ConversationColumns.FLAGS, CONVERSATION_FLAGS) 2223 .add(UIProvider.ConversationColumns.ACCOUNT_URI, 2224 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2225 .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) 2226 .build(); 2227 } 2228 return sMessageListMap; 2229 } 2230 private static ProjectionMap sMessageListMap; 2231 2232 /** 2233 * Generate UIProvider draft type; note the test for "reply all" must come before "reply" 2234 */ 2235 private static final String MESSAGE_DRAFT_TYPE = 2236 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_ORIGINAL + 2237 ") !=0 THEN " + UIProvider.DraftType.COMPOSE + 2238 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY_ALL + 2239 ") !=0 THEN " + UIProvider.DraftType.REPLY_ALL + 2240 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_REPLY + 2241 ") !=0 THEN " + UIProvider.DraftType.REPLY + 2242 " WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_TYPE_FORWARD + 2243 ") !=0 THEN " + UIProvider.DraftType.FORWARD + 2244 " ELSE " + UIProvider.DraftType.NOT_A_DRAFT + " END"; 2245 2246 private static final String MESSAGE_FLAGS = 2247 "CASE WHEN (" + MessageColumns.FLAGS + "&" + Message.FLAG_INCOMING_MEETING_INVITE + 2248 ") !=0 THEN " + UIProvider.MessageFlags.CALENDAR_INVITE + 2249 " ELSE 0 END"; 2250 2251 /** 2252 * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in 2253 * UnifiedEmail 2254 */ 2255 private static ProjectionMap getMessageViewMap() { 2256 if (sMessageViewMap == null) { 2257 sMessageViewMap = ProjectionMap.builder() 2258 .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) 2259 .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) 2260 .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) 2261 .add(UIProvider.MessageColumns.CONVERSATION_ID, 2262 uriWithFQId("uimessage", Message.TABLE_NAME)) 2263 .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) 2264 .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) 2265 .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) 2266 .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) 2267 .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) 2268 .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) 2269 .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) 2270 .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, 2271 EmailContent.MessageColumns.TIMESTAMP) 2272 .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) 2273 .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) 2274 .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") 2275 .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) 2276 .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") 2277 .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, 2278 EmailContent.MessageColumns.FLAG_ATTACHMENT) 2279 .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, 2280 uriWithFQId("uiattachments", Message.TABLE_NAME)) 2281 .add(UIProvider.MessageColumns.MESSAGE_FLAGS, MESSAGE_FLAGS) 2282 .add(UIProvider.MessageColumns.DRAFT_TYPE, MESSAGE_DRAFT_TYPE) 2283 .add(UIProvider.MessageColumns.MESSAGE_ACCOUNT_URI, 2284 uriWithColumn("uiaccount", MessageColumns.ACCOUNT_KEY)) 2285 .add(UIProvider.MessageColumns.STARRED, EmailContent.MessageColumns.FLAG_FAVORITE) 2286 .add(UIProvider.MessageColumns.READ, EmailContent.MessageColumns.FLAG_READ) 2287 .add(UIProvider.MessageColumns.SEEN, EmailContent.MessageColumns.FLAG_SEEN) 2288 .add(UIProvider.MessageColumns.SPAM_WARNING_STRING, null) 2289 .add(UIProvider.MessageColumns.SPAM_WARNING_LEVEL, 2290 Integer.toString(UIProvider.SpamWarningLevel.NO_WARNING)) 2291 .add(UIProvider.MessageColumns.SPAM_WARNING_LINK_TYPE, 2292 Integer.toString(UIProvider.SpamWarningLinkType.NO_LINK)) 2293 .add(UIProvider.MessageColumns.VIA_DOMAIN, null) 2294 .build(); 2295 } 2296 return sMessageViewMap; 2297 } 2298 private static ProjectionMap sMessageViewMap; 2299 2300 /** 2301 * Generate UIProvider folder capabilities from mailbox flags 2302 */ 2303 private static final String FOLDER_CAPABILITIES = 2304 "CASE WHEN (" + MailboxColumns.FLAGS + "&" + Mailbox.FLAG_ACCEPTS_MOVED_MAIL + 2305 ") !=0 THEN " + UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES + 2306 " ELSE 0 END"; 2307 2308 /** 2309 * Convert EmailProvider type to UIProvider type 2310 */ 2311 private static final String FOLDER_TYPE = "CASE " + MailboxColumns.TYPE 2312 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + UIProvider.FolderType.INBOX 2313 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + UIProvider.FolderType.DRAFT 2314 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + UIProvider.FolderType.OUTBOX 2315 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + UIProvider.FolderType.SENT 2316 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + UIProvider.FolderType.TRASH 2317 + " WHEN " + Mailbox.TYPE_JUNK + " THEN " + UIProvider.FolderType.SPAM 2318 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + UIProvider.FolderType.STARRED 2319 + " WHEN " + Mailbox.TYPE_UNREAD + " THEN " + UIProvider.FolderType.UNREAD 2320 + " WHEN " + Mailbox.TYPE_SEARCH + " THEN " 2321 + getFolderTypeFromMailboxType(Mailbox.TYPE_SEARCH) 2322 + " ELSE " + UIProvider.FolderType.DEFAULT + " END"; 2323 2324 private static final String FOLDER_ICON = "CASE " + MailboxColumns.TYPE 2325 + " WHEN " + Mailbox.TYPE_INBOX + " THEN " + R.drawable.ic_folder_inbox 2326 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN " + R.drawable.ic_folder_drafts 2327 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN " + R.drawable.ic_folder_outbox 2328 + " WHEN " + Mailbox.TYPE_SENT + " THEN " + R.drawable.ic_folder_sent 2329 + " WHEN " + Mailbox.TYPE_TRASH + " THEN " + R.drawable.ic_folder_trash 2330 + " WHEN " + Mailbox.TYPE_STARRED + " THEN " + R.drawable.ic_folder_star 2331 + " ELSE -1 END"; 2332 2333 /** 2334 * Local-only folders set totalCount < 0; such folders should substitute message count for 2335 * total count. 2336 * TODO: IMAP and POP don't adhere to this convention yet so for now we force a few types. 2337 */ 2338 private static final String TOTAL_COUNT = "CASE WHEN " 2339 + MailboxColumns.TOTAL_COUNT + "<0 OR " 2340 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_DRAFTS + " OR " 2341 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_OUTBOX + " OR " 2342 + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH 2343 + " THEN " + MailboxColumns.MESSAGE_COUNT 2344 + " ELSE " + MailboxColumns.TOTAL_COUNT + " END"; 2345 2346 private static ProjectionMap getFolderListMap() { 2347 if (sFolderListMap == null) { 2348 sFolderListMap = ProjectionMap.builder() 2349 .add(BaseColumns._ID, MailboxColumns.ID) 2350 .add(UIProvider.FolderColumns.PERSISTENT_ID, MailboxColumns.SERVER_ID) 2351 .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) 2352 .add(UIProvider.FolderColumns.NAME, "displayName") 2353 .add(UIProvider.FolderColumns.HAS_CHILDREN, 2354 MailboxColumns.FLAGS + "&" + Mailbox.FLAG_HAS_CHILDREN) 2355 .add(UIProvider.FolderColumns.CAPABILITIES, FOLDER_CAPABILITIES) 2356 .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") 2357 .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) 2358 .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uisubfolders")) 2359 .add(UIProvider.FolderColumns.UNREAD_COUNT, MailboxColumns.UNREAD_COUNT) 2360 .add(UIProvider.FolderColumns.TOTAL_COUNT, TOTAL_COUNT) 2361 .add(UIProvider.FolderColumns.REFRESH_URI, uriWithId(QUERY_UIREFRESH)) 2362 .add(UIProvider.FolderColumns.SYNC_STATUS, MailboxColumns.UI_SYNC_STATUS) 2363 .add(UIProvider.FolderColumns.LAST_SYNC_RESULT, MailboxColumns.UI_LAST_SYNC_RESULT) 2364 .add(UIProvider.FolderColumns.TYPE, FOLDER_TYPE) 2365 .add(UIProvider.FolderColumns.ICON_RES_ID, FOLDER_ICON) 2366 .add(UIProvider.FolderColumns.LOAD_MORE_URI, uriWithId("uiloadmore")) 2367 .add(UIProvider.FolderColumns.HIERARCHICAL_DESC, MailboxColumns.HIERARCHICAL_NAME) 2368 .add(UIProvider.FolderColumns.PARENT_URI, "case when " + MailboxColumns.PARENT_KEY 2369 + "=" + Mailbox.NO_MAILBOX + " then NULL else " + 2370 uriWithColumn("uifolder", MailboxColumns.PARENT_KEY) + " end") 2371 .build(); 2372 } 2373 return sFolderListMap; 2374 } 2375 private static ProjectionMap sFolderListMap; 2376 2377 /** 2378 * Constructs the map of default entries for accounts. These values can be overridden in 2379 * {@link #genQueryAccount(String[], String)}. 2380 */ 2381 private static ProjectionMap getAccountListMap(Context context) { 2382 if (sAccountListMap == null) { 2383 final MailPrefs mailPrefs = MailPrefs.get(context); 2384 2385 final ProjectionMap.Builder builder = ProjectionMap.builder() 2386 .add(BaseColumns._ID, AccountColumns.ID) 2387 .add(UIProvider.AccountColumns.FOLDER_LIST_URI, uriWithId("uifolders")) 2388 .add(UIProvider.AccountColumns.FULL_FOLDER_LIST_URI, uriWithId("uifullfolders")) 2389 .add(UIProvider.AccountColumns.ALL_FOLDER_LIST_URI, uriWithId("uiallfolders")) 2390 .add(UIProvider.AccountColumns.NAME, AccountColumns.DISPLAY_NAME) 2391 .add(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME, 2392 AccountColumns.EMAIL_ADDRESS) 2393 .add(UIProvider.AccountColumns.UNDO_URI, 2394 ("'content://" + EmailContent.AUTHORITY + "/uiundo'")) 2395 .add(UIProvider.AccountColumns.URI, uriWithId("uiaccount")) 2396 .add(UIProvider.AccountColumns.SEARCH_URI, uriWithId("uisearch")) 2397 // TODO: Is provider version used? 2398 .add(UIProvider.AccountColumns.PROVIDER_VERSION, "1") 2399 .add(UIProvider.AccountColumns.SYNC_STATUS, "0") 2400 .add(UIProvider.AccountColumns.RECENT_FOLDER_LIST_URI, 2401 uriWithId("uirecentfolders")) 2402 .add(UIProvider.AccountColumns.DEFAULT_RECENT_FOLDER_LIST_URI, 2403 uriWithId("uidefaultrecentfolders")) 2404 .add(UIProvider.AccountColumns.SettingsColumns.SIGNATURE, 2405 AccountColumns.SIGNATURE) 2406 .add(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS, 2407 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS)) 2408 .add(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE, "0") 2409 .add(UIProvider.AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, 2410 Integer.toString(UIProvider.ConversationViewMode.UNDEFINED)) 2411 .add(UIProvider.AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, null); 2412 2413 final String feedbackUri = context.getString(R.string.email_feedback_uri); 2414 if (!TextUtils.isEmpty(feedbackUri)) { 2415 // This string needs to be in single quotes, as it will be used as a constant 2416 // in a sql expression 2417 builder.add(UIProvider.AccountColumns.SEND_FEEDBACK_INTENT_URI, 2418 "'" + feedbackUri + "'"); 2419 } 2420 2421 sAccountListMap = builder.build(); 2422 } 2423 return sAccountListMap; 2424 } 2425 private static ProjectionMap sAccountListMap; 2426 2427 private static ProjectionMap getQuickResponseMap() { 2428 if (sQuickResponseMap == null) { 2429 sQuickResponseMap = ProjectionMap.builder() 2430 .add(UIProvider.QuickResponseColumns.TEXT, 2431 EmailContent.QuickResponseColumns.TEXT) 2432 .add(UIProvider.QuickResponseColumns.URI, 2433 "'" + combinedUriString("quickresponse", "") + "'||" 2434 + EmailContent.QuickResponseColumns.ID) 2435 .build(); 2436 } 2437 return sQuickResponseMap; 2438 } 2439 private static ProjectionMap sQuickResponseMap; 2440 2441 /** 2442 * The "ORDER BY" clause for top level folders 2443 */ 2444 private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE 2445 + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" 2446 + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" 2447 + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" 2448 + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" 2449 + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" 2450 + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" 2451 // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. 2452 + " ELSE 10 END" 2453 + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2454 2455 /** 2456 * Mapping of UIProvider columns to EmailProvider columns for a message's attachments 2457 */ 2458 private static ProjectionMap getAttachmentMap() { 2459 if (sAttachmentMap == null) { 2460 sAttachmentMap = ProjectionMap.builder() 2461 .add(UIProvider.AttachmentColumns.NAME, AttachmentColumns.FILENAME) 2462 .add(UIProvider.AttachmentColumns.SIZE, AttachmentColumns.SIZE) 2463 .add(UIProvider.AttachmentColumns.URI, uriWithId("uiattachment")) 2464 .add(UIProvider.AttachmentColumns.CONTENT_TYPE, AttachmentColumns.MIME_TYPE) 2465 .add(UIProvider.AttachmentColumns.STATE, AttachmentColumns.UI_STATE) 2466 .add(UIProvider.AttachmentColumns.DESTINATION, AttachmentColumns.UI_DESTINATION) 2467 .add(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, 2468 AttachmentColumns.UI_DOWNLOADED_SIZE) 2469 .add(UIProvider.AttachmentColumns.CONTENT_URI, AttachmentColumns.CONTENT_URI) 2470 .add(UIProvider.AttachmentColumns.FLAGS, AttachmentColumns.FLAGS) 2471 .build(); 2472 } 2473 return sAttachmentMap; 2474 } 2475 private static ProjectionMap sAttachmentMap; 2476 2477 /** 2478 * Generate the SELECT clause using a specified mapping and the original UI projection 2479 * @param map the ProjectionMap to use for this projection 2480 * @param projection the projection as sent by UnifiedEmail 2481 * @return a StringBuilder containing the SELECT expression for a SQLite query 2482 */ 2483 private static StringBuilder genSelect(ProjectionMap map, String[] projection) { 2484 return genSelect(map, projection, EMPTY_CONTENT_VALUES); 2485 } 2486 2487 private static StringBuilder genSelect(ProjectionMap map, String[] projection, 2488 ContentValues values) { 2489 final StringBuilder sb = new StringBuilder("SELECT "); 2490 boolean first = true; 2491 for (final String column: projection) { 2492 if (first) { 2493 first = false; 2494 } else { 2495 sb.append(','); 2496 } 2497 final String val; 2498 // First look at values; this is an override of default behavior 2499 if (values.containsKey(column)) { 2500 final String value = values.getAsString(column); 2501 if (value == null) { 2502 val = "NULL AS " + column; 2503 } else if (value.startsWith("@")) { 2504 val = value.substring(1) + " AS " + column; 2505 } else { 2506 val = "'" + value + "' AS " + column; 2507 } 2508 } else { 2509 // Now, get the standard value for the column from our projection map 2510 final String mapVal = map.get(column); 2511 // If we don't have the column, return "NULL AS <column>", and warn 2512 if (mapVal == null) { 2513 val = "NULL AS " + column; 2514 // Apparently there's a lot of these, so don't spam the log with warnings 2515 // LogUtils.w(TAG, "column " + column + " missing from projection map"); 2516 } else { 2517 val = mapVal; 2518 } 2519 } 2520 sb.append(val); 2521 } 2522 return sb; 2523 } 2524 2525 /** 2526 * Convenience method to create a Uri string given the "type" of query; we append the type 2527 * of the query and the id column name (_id) 2528 * 2529 * @param type the "type" of the query, as defined by our UriMatcher definitions 2530 * @return a Uri string 2531 */ 2532 private static String uriWithId(String type) { 2533 return uriWithColumn(type, EmailContent.RECORD_ID); 2534 } 2535 2536 /** 2537 * Convenience method to create a Uri string given the "type" of query; we append the type 2538 * of the query and the passed in column name 2539 * 2540 * @param type the "type" of the query, as defined by our UriMatcher definitions 2541 * @param columnName the column in the table being queried 2542 * @return a Uri string 2543 */ 2544 private static String uriWithColumn(String type, String columnName) { 2545 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + columnName; 2546 } 2547 2548 /** 2549 * Convenience method to create a Uri string given the "type" of query and the table name to 2550 * which it applies; we append the type of the query and the fully qualified (FQ) id column 2551 * (i.e. including the table name); we need this for join queries where _id would otherwise 2552 * be ambiguous 2553 * 2554 * @param type the "type" of the query, as defined by our UriMatcher definitions 2555 * @param tableName the name of the table whose _id is referred to 2556 * @return a Uri string 2557 */ 2558 private static String uriWithFQId(String type, String tableName) { 2559 return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; 2560 } 2561 2562 // Regex that matches start of img tag. '<(?i)img\s+'. 2563 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 2564 2565 /** 2566 * Class that holds the sqlite query and the attachment (JSON) value (which might be null) 2567 */ 2568 private static class MessageQuery { 2569 final String query; 2570 final String attachmentJson; 2571 2572 MessageQuery(String _query, String _attachmentJson) { 2573 query = _query; 2574 attachmentJson = _attachmentJson; 2575 } 2576 } 2577 2578 /** 2579 * Generate the "view message" SQLite query, given a projection from UnifiedEmail 2580 * 2581 * @param uiProjection as passed from UnifiedEmail 2582 * @return the SQLite query to be executed on the EmailProvider database 2583 */ 2584 private MessageQuery genQueryViewMessage(String[] uiProjection, String id) { 2585 Context context = getContext(); 2586 long messageId = Long.parseLong(id); 2587 Message msg = Message.restoreMessageWithId(context, messageId); 2588 ContentValues values = new ContentValues(); 2589 String attachmentJson = null; 2590 if (msg != null) { 2591 Body body = Body.restoreBodyWithMessageId(context, messageId); 2592 if (body != null) { 2593 if (body.mHtmlContent != null) { 2594 if (IMG_TAG_START_REGEX.matcher(body.mHtmlContent).find()) { 2595 values.put(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, 1); 2596 } 2597 } 2598 } 2599 Address[] fromList = Address.unpack(msg.mFrom); 2600 int autoShowImages = 0; 2601 final MailPrefs mailPrefs = MailPrefs.get(context); 2602 for (Address sender : fromList) { 2603 final String email = sender.getAddress(); 2604 if (mailPrefs.getDisplayImagesFromSender(email)) { 2605 autoShowImages = 1; 2606 break; 2607 } 2608 } 2609 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, autoShowImages); 2610 // Add attachments... 2611 Attachment[] atts = Attachment.restoreAttachmentsWithMessageId(context, messageId); 2612 if (atts.length > 0) { 2613 ArrayList<com.android.mail.providers.Attachment> uiAtts = 2614 new ArrayList<com.android.mail.providers.Attachment>(); 2615 for (Attachment att : atts) { 2616 // TODO: This code is intended to strip out any inlined attachments (which 2617 // would have a non-null contentId) so that they will not display at the bottom 2618 // along with the non-inlined attachments. 2619 // The problem is that the UI_ATTACHMENTS query does not behave the same way, 2620 // which causes crazy formatting. 2621 // There is an open question here, should attachments that are inlined 2622 // ALSO appear in the list of attachments at the bottom with the non-inlined 2623 // attachments? 2624 // Either way, the two queries need to behave the same way. 2625 // As of now, they will. If we decide to stop this, then we need to enable 2626 // the code below, and then also make the UI_ATTACHMENTS query behave 2627 // the same way. 2628// 2629// if (att.mContentId != null && att.getContentUri() != null) { 2630// continue; 2631// } 2632 com.android.mail.providers.Attachment uiAtt = 2633 new com.android.mail.providers.Attachment(); 2634 uiAtt.setName(att.mFileName); 2635 uiAtt.setContentType(att.mMimeType); 2636 uiAtt.size = (int) att.mSize; 2637 uiAtt.uri = uiUri("uiattachment", att.mId); 2638 uiAtt.flags = att.mFlags; 2639 uiAtts.add(uiAtt); 2640 } 2641 values.put(UIProvider.MessageColumns.ATTACHMENTS, "@?"); // @ for literal 2642 attachmentJson = com.android.mail.providers.Attachment.toJSONArray(uiAtts); 2643 } 2644 if (msg.mDraftInfo != 0) { 2645 values.put(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, 2646 (msg.mDraftInfo & Message.DRAFT_INFO_APPEND_REF_MESSAGE) != 0 ? 1 : 0); 2647 values.put(UIProvider.MessageColumns.QUOTE_START_POS, 2648 msg.mDraftInfo & Message.DRAFT_INFO_QUOTE_POS_MASK); 2649 } 2650 if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) { 2651 values.put(UIProvider.MessageColumns.EVENT_INTENT_URI, 2652 "content://ui.email2.android.com/event/" + msg.mId); 2653 } 2654 } 2655 StringBuilder sb = genSelect(getMessageViewMap(), uiProjection, values); 2656 sb.append(" FROM " + Message.TABLE_NAME + " LEFT JOIN " + Body.TABLE_NAME + " ON " + 2657 Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " WHERE " + 2658 Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); 2659 String sql = sb.toString(); 2660 return new MessageQuery(sql, attachmentJson); 2661 } 2662 2663 private static void appendConversationInfoColumns(final StringBuilder stringBuilder) { 2664 // TODO(skennedy) These columns are needed for the respond call for ConversationInfo :( 2665 // There may be a better way to do this, but since the projection is specified by the 2666 // unified UI code, it can't ask for these columns. 2667 stringBuilder.append(',').append(MessageColumns.DISPLAY_NAME) 2668 .append(',').append(MessageColumns.FROM_LIST); 2669 } 2670 2671 /** 2672 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2673 * 2674 * @param uiProjection as passed from UnifiedEmail 2675 * @param unseenOnly <code>true</code> to only return unseen messages 2676 * @return the SQLite query to be executed on the EmailProvider database 2677 */ 2678 private static String genQueryMailboxMessages(String[] uiProjection, final boolean unseenOnly) { 2679 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2680 appendConversationInfoColumns(sb); 2681 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2682 Message.FLAG_LOADED_SELECTION + " AND " + 2683 Message.MAILBOX_KEY + "=? "); 2684 if (unseenOnly) { 2685 sb.append("AND ").append(MessageColumns.FLAG_SEEN).append(" = 0 "); 2686 sb.append("AND ").append(MessageColumns.FLAG_READ).append(" = 0 "); 2687 } 2688 sb.append("ORDER BY " + MessageColumns.TIMESTAMP + " DESC "); 2689 sb.append("LIMIT " + UIProvider.CONVERSATION_PROJECTION_QUERY_CURSOR_WINDOW_LIMT); 2690 return sb.toString(); 2691 } 2692 2693 /** 2694 * Generate various virtual mailbox SQLite queries, given a projection from UnifiedEmail 2695 * 2696 * @param uiProjection as passed from UnifiedEmail 2697 * @param mailboxId the id of the virtual mailbox 2698 * @param unseenOnly <code>true</code> to only return unseen messages 2699 * @return the SQLite query to be executed on the EmailProvider database 2700 */ 2701 private static Cursor getVirtualMailboxMessagesCursor(SQLiteDatabase db, String[] uiProjection, 2702 long mailboxId, final boolean unseenOnly) { 2703 ContentValues values = new ContentValues(); 2704 values.put(UIProvider.ConversationColumns.COLOR, CONVERSATION_COLOR); 2705 final int virtualMailboxId = getVirtualMailboxType(mailboxId); 2706 final String[] selectionArgs; 2707 StringBuilder sb = genSelect(getMessageListMap(), uiProjection, values); 2708 appendConversationInfoColumns(sb); 2709 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + 2710 Message.FLAG_LOADED_SELECTION + " AND "); 2711 if (isCombinedMailbox(mailboxId)) { 2712 if (unseenOnly) { 2713 sb.append(MessageColumns.FLAG_SEEN).append("=0 AND "); 2714 sb.append(MessageColumns.FLAG_READ).append("=0 AND "); 2715 } 2716 selectionArgs = null; 2717 } else { 2718 if (virtualMailboxId == Mailbox.TYPE_INBOX) { 2719 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2720 } 2721 sb.append(MessageColumns.ACCOUNT_KEY).append("=? AND "); 2722 selectionArgs = new String[]{getVirtualMailboxAccountIdString(mailboxId)}; 2723 } 2724 switch (getVirtualMailboxType(mailboxId)) { 2725 case Mailbox.TYPE_INBOX: 2726 sb.append(MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID + 2727 " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + 2728 "=" + Mailbox.TYPE_INBOX + ")"); 2729 break; 2730 case Mailbox.TYPE_STARRED: 2731 sb.append(MessageColumns.FLAG_FAVORITE + "=1"); 2732 break; 2733 case Mailbox.TYPE_UNREAD: 2734 sb.append(MessageColumns.FLAG_READ + "=0 AND " + MessageColumns.MAILBOX_KEY + 2735 " NOT IN (SELECT " + MailboxColumns.ID + " FROM " + Mailbox.TABLE_NAME + 2736 " WHERE " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_TRASH + ")"); 2737 break; 2738 default: 2739 throw new IllegalArgumentException("No virtual mailbox for: " + mailboxId); 2740 } 2741 sb.append(" ORDER BY " + MessageColumns.TIMESTAMP + " DESC"); 2742 return db.rawQuery(sb.toString(), selectionArgs); 2743 } 2744 2745 /** 2746 * Generate the "message list" SQLite query, given a projection from UnifiedEmail 2747 * 2748 * @param uiProjection as passed from UnifiedEmail 2749 * @return the SQLite query to be executed on the EmailProvider database 2750 */ 2751 private static String genQueryConversation(String[] uiProjection) { 2752 StringBuilder sb = genSelect(getMessageListMap(), uiProjection); 2753 sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.RECORD_ID + "=?"); 2754 return sb.toString(); 2755 } 2756 2757 /** 2758 * Generate the "top level folder list" SQLite query, given a projection from UnifiedEmail 2759 * 2760 * @param uiProjection as passed from UnifiedEmail 2761 * @return the SQLite query to be executed on the EmailProvider database 2762 */ 2763 private static String genQueryAccountMailboxes(String[] uiProjection) { 2764 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2765 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2766 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2767 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2768 " AND " + MailboxColumns.PARENT_KEY + " < 0 ORDER BY "); 2769 sb.append(MAILBOX_ORDER_BY); 2770 return sb.toString(); 2771 } 2772 2773 /** 2774 * Generate the "all folders" SQLite query, given a projection from UnifiedEmail. The list is 2775 * sorted by the name as it appears in a hierarchical listing 2776 * 2777 * @param uiProjection as passed from UnifiedEmail 2778 * @return the SQLite query to be executed on the EmailProvider database 2779 */ 2780 private static String genQueryAccountAllMailboxes(String[] uiProjection) { 2781 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2782 // Use a derived column to choose either hierarchicalName or displayName 2783 sb.append(", case when " + MailboxColumns.HIERARCHICAL_NAME + " is null then " + 2784 MailboxColumns.DISPLAY_NAME + " else " + MailboxColumns.HIERARCHICAL_NAME + 2785 " end as h_name"); 2786 // Order by the derived column 2787 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2788 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2789 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2790 " ORDER BY h_name"); 2791 return sb.toString(); 2792 } 2793 2794 /** 2795 * Generate the "recent folder list" SQLite query, given a projection from UnifiedEmail 2796 * 2797 * @param uiProjection as passed from UnifiedEmail 2798 * @return the SQLite query to be executed on the EmailProvider database 2799 */ 2800 private static String genQueryRecentMailboxes(String[] uiProjection) { 2801 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 2802 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ACCOUNT_KEY + 2803 "=? AND " + MailboxColumns.TYPE + " < " + Mailbox.TYPE_NOT_EMAIL + 2804 " AND " + MailboxColumns.TYPE + " != " + Mailbox.TYPE_SEARCH + 2805 " AND " + MailboxColumns.PARENT_KEY + " < 0 AND " + 2806 MailboxColumns.LAST_TOUCHED_TIME + " > 0 ORDER BY " + 2807 MailboxColumns.LAST_TOUCHED_TIME + " DESC"); 2808 return sb.toString(); 2809 } 2810 2811 private static int getFolderCapabilities(EmailServiceInfo info, int type, long mailboxId) { 2812 // Special case for Search folders: only permit delete, do not try to give any other caps. 2813 if (type == Mailbox.TYPE_SEARCH) { 2814 return UIProvider.FolderCapabilities.DELETE; 2815 } 2816 2817 // All folders support delete, except drafts. 2818 int caps = 0; 2819 if (type != Mailbox.TYPE_DRAFTS) { 2820 caps = UIProvider.FolderCapabilities.DELETE; 2821 } 2822 if (info != null && info.offerLookback) { 2823 // Protocols supporting lookback support settings 2824 caps |= UIProvider.FolderCapabilities.SUPPORTS_SETTINGS; 2825 } 2826 2827 if (type == Mailbox.TYPE_MAIL || type == Mailbox.TYPE_TRASH || 2828 type == Mailbox.TYPE_JUNK || type == Mailbox.TYPE_INBOX) { 2829 // If the mailbox can accept moved mail, report that as well 2830 caps |= UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES; 2831 caps |= UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION; 2832 } 2833 2834 // For trash, we don't allow undo 2835 if (type == Mailbox.TYPE_TRASH) { 2836 caps = UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES | 2837 UIProvider.FolderCapabilities.ALLOWS_REMOVE_CONVERSATION | 2838 UIProvider.FolderCapabilities.DELETE | 2839 UIProvider.FolderCapabilities.DELETE_ACTION_FINAL; 2840 } 2841 if (isVirtualMailbox(mailboxId)) { 2842 caps |= UIProvider.FolderCapabilities.IS_VIRTUAL; 2843 } 2844 return caps; 2845 } 2846 2847 /** 2848 * Generate a "single mailbox" SQLite query, given a projection from UnifiedEmail 2849 * 2850 * @param uiProjection as passed from UnifiedEmail 2851 * @return the SQLite query to be executed on the EmailProvider database 2852 */ 2853 private String genQueryMailbox(String[] uiProjection, String id) { 2854 long mailboxId = Long.parseLong(id); 2855 ContentValues values = new ContentValues(3); 2856 if (mSearchParams != null && mailboxId == mSearchParams.mSearchMailboxId) { 2857 // "load more" is valid for search results 2858 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2859 uiUriString("uiloadmore", mailboxId)); 2860 values.put(UIProvider.FolderColumns.CAPABILITIES, UIProvider.FolderCapabilities.DELETE); 2861 } else { 2862 Context context = getContext(); 2863 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 2864 // Make sure we can't get NPE if mailbox has disappeared (the result will end up moot) 2865 if (mailbox != null) { 2866 String protocol = Account.getProtocol(context, mailbox.mAccountKey); 2867 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 2868 // All folders support delete 2869 if (info != null && info.offerLoadMore) { 2870 // "load more" is valid for protocols not supporting "lookback" 2871 values.put(UIProvider.FolderColumns.LOAD_MORE_URI, 2872 uiUriString("uiloadmore", mailboxId)); 2873 } 2874 values.put(UIProvider.FolderColumns.CAPABILITIES, 2875 getFolderCapabilities(info, mailbox.mType, mailboxId)); 2876 // The persistent id is used to form a filename, so we must ensure that it doesn't 2877 // include illegal characters (such as '/'). Only perform the encoding if this 2878 // query wants the persistent id. 2879 boolean shouldEncodePersistentId = false; 2880 if (uiProjection == null) { 2881 shouldEncodePersistentId = true; 2882 } else { 2883 for (final String column : uiProjection) { 2884 if (TextUtils.equals(column, UIProvider.FolderColumns.PERSISTENT_ID)) { 2885 shouldEncodePersistentId = true; 2886 break; 2887 } 2888 } 2889 } 2890 if (shouldEncodePersistentId) { 2891 values.put(UIProvider.FolderColumns.PERSISTENT_ID, 2892 Base64.encodeToString(mailbox.mServerId.getBytes(), 2893 Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); 2894 } 2895 } 2896 } 2897 StringBuilder sb = genSelect(getFolderListMap(), uiProjection, values); 2898 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.ID + "=?"); 2899 return sb.toString(); 2900 } 2901 2902 public static final String LEGACY_AUTHORITY = "ui.email.android.com"; 2903 private static final Uri BASE_EXTERNAL_URI = Uri.parse("content://" + LEGACY_AUTHORITY); 2904 2905 private static final Uri BASE_EXTERAL_URI2 = Uri.parse("content://ui.email2.android.com"); 2906 2907 private static String getExternalUriString(String segment, String account) { 2908 return BASE_EXTERNAL_URI.buildUpon().appendPath(segment) 2909 .appendQueryParameter("account", account).build().toString(); 2910 } 2911 2912 private static String getExternalUriStringEmail2(String segment, String account) { 2913 return BASE_EXTERAL_URI2.buildUpon().appendPath(segment) 2914 .appendQueryParameter("account", account).build().toString(); 2915 } 2916 2917 private static String getBits(int bitField) { 2918 StringBuilder sb = new StringBuilder(" "); 2919 for (int i = 0; i < 32; i++, bitField >>= 1) { 2920 if ((bitField & 1) != 0) { 2921 sb.append(i) 2922 .append(" "); 2923 } 2924 } 2925 return sb.toString(); 2926 } 2927 2928 private static int getCapabilities(Context context, long accountId) { 2929 final EmailServiceProxy service = 2930 EmailServiceUtils.getServiceForAccount(context, accountId); 2931 int capabilities = 0; 2932 Account acct = null; 2933 try { 2934 service.setTimeout(10); 2935 acct = Account.restoreAccountWithId(context, accountId); 2936 if (acct == null) { 2937 LogUtils.d(TAG, "getCapabilities() for " + accountId 2938 + ": returning 0x0 (no account)"); 2939 return 0; 2940 } 2941 capabilities = service.getCapabilities(acct); 2942 LogUtils.d(TAG, "getCapabilities() for %s: 0x%x %s", 2943 LogUtils.sanitizeAccountName(acct.mDisplayName), 2944 capabilities, getBits(capabilities)); 2945 } catch (RemoteException e) { 2946 // Nothing to do 2947 LogUtils.w(TAG, "getCapabilities() for " + acct.mDisplayName + ": RemoteException"); 2948 } 2949 2950 // If the configuration states that feedback is supported, add that capability 2951 final Resources res = context.getResources(); 2952 if (res.getBoolean(R.bool.feedback_supported)) { 2953 capabilities |= UIProvider.AccountCapabilities.SEND_FEEDBACK; 2954 } 2955 2956 capabilities |= UIProvider.AccountCapabilities.NESTED_FOLDERS; 2957 2958 return capabilities; 2959 } 2960 2961 /** 2962 * Generate a "single account" SQLite query, given a projection from UnifiedEmail 2963 * 2964 * @param uiProjection as passed from UnifiedEmail 2965 * @param id account row ID 2966 * @return the SQLite query to be executed on the EmailProvider database 2967 */ 2968 private String genQueryAccount(String[] uiProjection, String id) { 2969 final ContentValues values = new ContentValues(); 2970 final long accountId = Long.parseLong(id); 2971 final Context context = getContext(); 2972 2973 EmailServiceInfo info = null; 2974 2975 // TODO: If uiProjection is null, this will NPE. We should do everything here if it's null. 2976 final Set<String> projectionColumns = ImmutableSet.copyOf(uiProjection); 2977 2978 if (projectionColumns.contains(UIProvider.AccountColumns.CAPABILITIES)) { 2979 // Get account capabilities from the service 2980 values.put(UIProvider.AccountColumns.CAPABILITIES, getCapabilities(context, accountId)); 2981 } 2982 if (projectionColumns.contains(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 2983 values.put(UIProvider.AccountColumns.SETTINGS_INTENT_URI, 2984 getExternalUriString("settings", id)); 2985 } 2986 if (projectionColumns.contains(UIProvider.AccountColumns.COMPOSE_URI)) { 2987 values.put(UIProvider.AccountColumns.COMPOSE_URI, 2988 getExternalUriStringEmail2("compose", id)); 2989 } 2990 if (projectionColumns.contains(UIProvider.AccountColumns.MIME_TYPE)) { 2991 values.put(UIProvider.AccountColumns.MIME_TYPE, EMAIL_APP_MIME_TYPE); 2992 } 2993 if (projectionColumns.contains(UIProvider.AccountColumns.COLOR)) { 2994 values.put(UIProvider.AccountColumns.COLOR, ACCOUNT_COLOR); 2995 } 2996 2997 final Preferences prefs = Preferences.getPreferences(getContext()); 2998 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 2999 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 3000 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE, 3001 prefs.getConfirmDelete() ? "1" : "0"); 3002 } 3003 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 3004 values.put(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND, 3005 prefs.getConfirmSend() ? "1" : "0"); 3006 } 3007 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.SWIPE)) { 3008 values.put(UIProvider.AccountColumns.SettingsColumns.SWIPE, 3009 mailPrefs.getConversationListSwipeActionInteger(false)); 3010 } 3011 if (projectionColumns.contains( 3012 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 3013 values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON, 3014 getConversationListIcon(mailPrefs)); 3015 } 3016 if (projectionColumns.contains( 3017 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) { 3018 values.put(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS, 3019 "0"); 3020 } 3021 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 3022 int autoAdvance = prefs.getAutoAdvanceDirection(); 3023 values.put(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE, 3024 autoAdvanceToUiValue(autoAdvance)); 3025 } 3026 if (projectionColumns.contains( 3027 UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 3028 int textZoom = prefs.getTextZoom(); 3029 values.put(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, 3030 textZoomToUiValue(textZoom)); 3031 } 3032 // Set default inbox, if we've got an inbox; otherwise, say initial sync needed 3033 final long inboxMailboxId = 3034 Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_INBOX); 3035 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX) && 3036 inboxMailboxId != Mailbox.NO_MAILBOX) { 3037 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX, 3038 uiUriString("uifolder", inboxMailboxId)); 3039 } 3040 if (projectionColumns.contains( 3041 UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME) && 3042 inboxMailboxId != Mailbox.NO_MAILBOX) { 3043 values.put(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX_NAME, 3044 Mailbox.getDisplayName(context, inboxMailboxId)); 3045 } 3046 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_STATUS)) { 3047 if (inboxMailboxId != Mailbox.NO_MAILBOX) { 3048 values.put(UIProvider.AccountColumns.SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 3049 } else { 3050 values.put(UIProvider.AccountColumns.SYNC_STATUS, 3051 UIProvider.SyncStatus.INITIAL_SYNC_NEEDED); 3052 } 3053 } 3054 if (projectionColumns.contains( 3055 UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED)) { 3056 // Email doesn't support priority inbox, so always state priority arrows disabled. 3057 values.put(UIProvider.AccountColumns.SettingsColumns.PRIORITY_ARROWS_ENABLED, "0"); 3058 } 3059 if (projectionColumns.contains( 3060 UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI)) { 3061 // Set the setup intent if needed 3062 // TODO We should clarify/document the trash/setup relationship 3063 long trashId = Mailbox.findMailboxOfType(context, accountId, Mailbox.TYPE_TRASH); 3064 if (trashId == Mailbox.NO_MAILBOX) { 3065 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 3066 if (info != null && info.requiresSetup) { 3067 values.put(UIProvider.AccountColumns.SettingsColumns.SETUP_INTENT_URI, 3068 getExternalUriString("setup", id)); 3069 } 3070 } 3071 } 3072 if (projectionColumns.contains(UIProvider.AccountColumns.TYPE)) { 3073 final String type; 3074 if (info == null) { 3075 info = EmailServiceUtils.getServiceInfoForAccount(context, accountId); 3076 } 3077 if (info != null) { 3078 type = info.accountType; 3079 } else { 3080 type = "unknown"; 3081 } 3082 3083 values.put(UIProvider.AccountColumns.TYPE, type); 3084 } 3085 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX) && 3086 inboxMailboxId != Mailbox.NO_MAILBOX) { 3087 values.put(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX, 3088 uiUriString("uifolder", inboxMailboxId)); 3089 } 3090 if (projectionColumns.contains(UIProvider.AccountColumns.SYNC_AUTHORITY)) { 3091 values.put(UIProvider.AccountColumns.SYNC_AUTHORITY, EmailContent.AUTHORITY); 3092 } 3093 if (projectionColumns.contains(UIProvider.AccountColumns.QUICK_RESPONSE_URI)) { 3094 values.put(UIProvider.AccountColumns.QUICK_RESPONSE_URI, 3095 combinedUriString("quickresponse/account", id)); 3096 } 3097 if (projectionColumns.contains(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 3098 values.put(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR, 3099 mailPrefs.getDefaultReplyAll() 3100 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 3101 : UIProvider.DefaultReplyBehavior.REPLY); 3102 } 3103 3104 final StringBuilder sb = genSelect(getAccountListMap(getContext()), uiProjection, values); 3105 sb.append(" FROM " + Account.TABLE_NAME + " WHERE " + AccountColumns.ID + "=?"); 3106 return sb.toString(); 3107 } 3108 3109 private static int autoAdvanceToUiValue(int autoAdvance) { 3110 switch(autoAdvance) { 3111 case Preferences.AUTO_ADVANCE_OLDER: 3112 return UIProvider.AutoAdvance.OLDER; 3113 case Preferences.AUTO_ADVANCE_NEWER: 3114 return UIProvider.AutoAdvance.NEWER; 3115 case Preferences.AUTO_ADVANCE_MESSAGE_LIST: 3116 default: 3117 return UIProvider.AutoAdvance.LIST; 3118 } 3119 } 3120 3121 private static int textZoomToUiValue(int textZoom) { 3122 switch(textZoom) { 3123 case Preferences.TEXT_ZOOM_HUGE: 3124 return UIProvider.MessageTextSize.HUGE; 3125 case Preferences.TEXT_ZOOM_LARGE: 3126 return UIProvider.MessageTextSize.LARGE; 3127 case Preferences.TEXT_ZOOM_NORMAL: 3128 return UIProvider.MessageTextSize.NORMAL; 3129 case Preferences.TEXT_ZOOM_SMALL: 3130 return UIProvider.MessageTextSize.SMALL; 3131 case Preferences.TEXT_ZOOM_TINY: 3132 return UIProvider.MessageTextSize.TINY; 3133 default: 3134 return UIProvider.MessageTextSize.NORMAL; 3135 } 3136 } 3137 3138 /** 3139 * Generate a Uri string for a combined mailbox uri 3140 * @param type the uri command type (e.g. "uimessages") 3141 * @param id the id of the item (e.g. an account, mailbox, or message id) 3142 * @return a Uri string 3143 */ 3144 private static String combinedUriString(String type, String id) { 3145 return "content://" + EmailContent.AUTHORITY + "/" + type + "/" + id; 3146 } 3147 3148 public static final long COMBINED_ACCOUNT_ID = 0x10000000; 3149 3150 /** 3151 * Generate an id for a combined mailbox of a given type 3152 * @param type the mailbox type for the combined mailbox 3153 * @return the id, as a String 3154 */ 3155 private static String combinedMailboxId(int type) { 3156 return Long.toString(Account.ACCOUNT_ID_COMBINED_VIEW + type); 3157 } 3158 3159 public static long getVirtualMailboxId(long accountId, int type) { 3160 return (accountId << 32) + type; 3161 } 3162 3163 private static boolean isVirtualMailbox(long mailboxId) { 3164 return mailboxId >= 0x100000000L; 3165 } 3166 3167 private static boolean isCombinedMailbox(long mailboxId) { 3168 return (mailboxId >> 32) == COMBINED_ACCOUNT_ID; 3169 } 3170 3171 private static long getVirtualMailboxAccountId(long mailboxId) { 3172 return mailboxId >> 32; 3173 } 3174 3175 private static String getVirtualMailboxAccountIdString(long mailboxId) { 3176 return Long.toString(mailboxId >> 32); 3177 } 3178 3179 private static int getVirtualMailboxType(long mailboxId) { 3180 return (int)(mailboxId & 0xF); 3181 } 3182 3183 private void addCombinedAccountRow(MatrixCursor mc) { 3184 final long lastUsedAccountId = 3185 Preferences.getPreferences(getContext()).getLastUsedAccountId(); 3186 final long id = Account.getDefaultAccountId(getContext(), lastUsedAccountId); 3187 if (id == Account.NO_ACCOUNT) return; 3188 3189 // Build a map of the requested columns to the appropriate positions 3190 final ImmutableMap.Builder<String, Integer> builder = 3191 new ImmutableMap.Builder<String, Integer>(); 3192 final String[] columnNames = mc.getColumnNames(); 3193 for (int i = 0; i < columnNames.length; i++) { 3194 builder.put(columnNames[i], i); 3195 } 3196 final Map<String, Integer> colPosMap = builder.build(); 3197 3198 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 3199 3200 final Object[] values = new Object[columnNames.length]; 3201 if (colPosMap.containsKey(BaseColumns._ID)) { 3202 values[colPosMap.get(BaseColumns._ID)] = 0; 3203 } 3204 if (colPosMap.containsKey(UIProvider.AccountColumns.CAPABILITIES)) { 3205 values[colPosMap.get(UIProvider.AccountColumns.CAPABILITIES)] = 3206 AccountCapabilities.UNDO | AccountCapabilities.SENDING_UNAVAILABLE; 3207 } 3208 if (colPosMap.containsKey(UIProvider.AccountColumns.FOLDER_LIST_URI)) { 3209 values[colPosMap.get(UIProvider.AccountColumns.FOLDER_LIST_URI)] = 3210 combinedUriString("uifolders", COMBINED_ACCOUNT_ID_STRING); 3211 } 3212 if (colPosMap.containsKey(UIProvider.AccountColumns.NAME)) { 3213 values[colPosMap.get(UIProvider.AccountColumns.NAME)] = getContext().getString( 3214 R.string.mailbox_list_account_selector_combined_view); 3215 } 3216 if (colPosMap.containsKey(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)) { 3217 values[colPosMap.get(UIProvider.AccountColumns.ACCOUNT_MANAGER_NAME)] = 3218 getContext().getString(R.string.mailbox_list_account_selector_combined_view); 3219 } 3220 if (colPosMap.containsKey(UIProvider.AccountColumns.TYPE)) { 3221 values[colPosMap.get(UIProvider.AccountColumns.TYPE)] = "unknown"; 3222 } 3223 if (colPosMap.containsKey(UIProvider.AccountColumns.UNDO_URI)) { 3224 values[colPosMap.get(UIProvider.AccountColumns.UNDO_URI)] = 3225 "'content://" + EmailContent.AUTHORITY + "/uiundo'"; 3226 } 3227 if (colPosMap.containsKey(UIProvider.AccountColumns.URI)) { 3228 values[colPosMap.get(UIProvider.AccountColumns.URI)] = 3229 combinedUriString("uiaccount", COMBINED_ACCOUNT_ID_STRING); 3230 } 3231 if (colPosMap.containsKey(UIProvider.AccountColumns.MIME_TYPE)) { 3232 values[colPosMap.get(UIProvider.AccountColumns.MIME_TYPE)] = 3233 EMAIL_APP_MIME_TYPE; 3234 } 3235 if (colPosMap.containsKey(UIProvider.AccountColumns.SETTINGS_INTENT_URI)) { 3236 values[colPosMap.get(UIProvider.AccountColumns.SETTINGS_INTENT_URI)] = 3237 getExternalUriString("settings", COMBINED_ACCOUNT_ID_STRING); 3238 } 3239 if (colPosMap.containsKey(UIProvider.AccountColumns.COMPOSE_URI)) { 3240 values[colPosMap.get(UIProvider.AccountColumns.COMPOSE_URI)] = 3241 getExternalUriStringEmail2("compose", Long.toString(id)); 3242 } 3243 3244 // TODO: Get these from default account? 3245 Preferences prefs = Preferences.getPreferences(getContext()); 3246 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)) { 3247 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.AUTO_ADVANCE)] = 3248 Integer.toString(UIProvider.AutoAdvance.NEWER); 3249 } 3250 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)) { 3251 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE)] = 3252 Integer.toString(UIProvider.MessageTextSize.NORMAL); 3253 } 3254 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)) { 3255 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.SNAP_HEADERS)] = 3256 Integer.toString(UIProvider.SnapHeaderValue.ALWAYS); 3257 } 3258 //.add(UIProvider.SettingsColumns.SIGNATURE, AccountColumns.SIGNATURE) 3259 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)) { 3260 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.REPLY_BEHAVIOR)] = 3261 Integer.toString(mailPrefs.getDefaultReplyAll() 3262 ? UIProvider.DefaultReplyBehavior.REPLY_ALL 3263 : UIProvider.DefaultReplyBehavior.REPLY); 3264 } 3265 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)) { 3266 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ICON)] = 3267 getConversationListIcon(mailPrefs); 3268 } 3269 if (colPosMap.containsKey( 3270 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)) { 3271 values[colPosMap.get( 3272 UIProvider.AccountColumns.SettingsColumns.CONV_LIST_ATTACHMENT_PREVIEWS)] = 0; 3273 } 3274 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)) { 3275 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_DELETE)] = 3276 prefs.getConfirmDelete() ? 1 : 0; 3277 } 3278 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)) { 3279 values[colPosMap.get( 3280 UIProvider.AccountColumns.SettingsColumns.CONFIRM_ARCHIVE)] = 0; 3281 } 3282 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)) { 3283 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.CONFIRM_SEND)] = 3284 prefs.getConfirmSend() ? 1 : 0; 3285 } 3286 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)) { 3287 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.DEFAULT_INBOX)] = 3288 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 3289 } 3290 if (colPosMap.containsKey(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)) { 3291 values[colPosMap.get(UIProvider.AccountColumns.SettingsColumns.MOVE_TO_INBOX)] = 3292 combinedUriString("uifolder", combinedMailboxId(Mailbox.TYPE_INBOX)); 3293 } 3294 3295 mc.addRow(values); 3296 } 3297 3298 private static int getConversationListIcon(MailPrefs mailPrefs) { 3299 return mailPrefs.getShowSenderImages() ? 3300 UIProvider.ConversationListIcon.SENDER_IMAGE : 3301 UIProvider.ConversationListIcon.NONE; 3302 } 3303 3304 // TODO: Pass in projection b/10912870 3305 private Cursor getVirtualMailboxCursor(long mailboxId) { 3306 MatrixCursor mc = new MatrixCursorWithCachedColumns(UIProvider.FOLDERS_PROJECTION, 1); 3307 mc.addRow(getVirtualMailboxRow(getVirtualMailboxAccountId(mailboxId), 3308 getVirtualMailboxType(mailboxId))); 3309 return mc; 3310 } 3311 3312 // TODO: Pass in projection b/10912870 3313 private Object[] getVirtualMailboxRow(long accountId, int mailboxType) { 3314 final long id = getVirtualMailboxId(accountId, mailboxType); 3315 final String idString = Long.toString(id); 3316 Object[] values = new Object[UIProvider.FOLDERS_PROJECTION.length]; 3317 values[UIProvider.FOLDER_ID_COLUMN] = id; 3318 values[UIProvider.FOLDER_URI_COLUMN] = combinedUriString("uifolder", idString); 3319 values[UIProvider.FOLDER_NAME_COLUMN] = getFolderDisplayName( 3320 getFolderTypeFromMailboxType(mailboxType), ""); 3321 // default empty string since all of these should use resource strings 3322 values[UIProvider.FOLDER_HAS_CHILDREN_COLUMN] = 0; 3323 values[UIProvider.FOLDER_CAPABILITIES_COLUMN] = 3324 UIProvider.FolderCapabilities.DELETE | UIProvider.FolderCapabilities.IS_VIRTUAL; 3325 values[UIProvider.FOLDER_CONVERSATION_LIST_URI_COLUMN] = combinedUriString("uimessages", 3326 idString); 3327 3328 // Do any special handling 3329 final String accountIdString = Long.toString(accountId); 3330 switch (mailboxType) { 3331 case Mailbox.TYPE_INBOX: 3332 if (accountId == COMBINED_ACCOUNT_ID) { 3333 // Add the unread count 3334 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3335 MessageColumns.MAILBOX_KEY + " IN (SELECT " + MailboxColumns.ID 3336 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE 3337 + "=" + Mailbox.TYPE_INBOX + ") AND " + MessageColumns.FLAG_READ + "=0", 3338 null); 3339 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 3340 } 3341 // Add the icon 3342 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_inbox; 3343 break; 3344 case Mailbox.TYPE_UNREAD: { 3345 // Add the unread count 3346 final String accountKeyClause; 3347 final String[] whereArgs; 3348 if (accountId == COMBINED_ACCOUNT_ID) { 3349 accountKeyClause = ""; 3350 whereArgs = null; 3351 } else { 3352 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 3353 whereArgs = new String[] { accountIdString }; 3354 } 3355 final int unreadCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3356 accountKeyClause + MessageColumns.FLAG_READ + "=0 AND " 3357 + MessageColumns.MAILBOX_KEY + " NOT IN (SELECT " + MailboxColumns.ID 3358 + " FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.TYPE + "=" 3359 + Mailbox.TYPE_TRASH + ")", whereArgs); 3360 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = unreadCount; 3361 // Add the icon 3362 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_unread; 3363 break; 3364 } case Mailbox.TYPE_STARRED: { 3365 // Add the starred count as the unread count 3366 final String accountKeyClause; 3367 final String[] whereArgs; 3368 if (accountId == COMBINED_ACCOUNT_ID) { 3369 accountKeyClause = ""; 3370 whereArgs = null; 3371 } else { 3372 accountKeyClause = MessageColumns.ACCOUNT_KEY + "= ? AND "; 3373 whereArgs = new String[] { accountIdString }; 3374 } 3375 final int starredCount = EmailContent.count(getContext(), Message.CONTENT_URI, 3376 accountKeyClause + MessageColumns.FLAG_FAVORITE + "=1", whereArgs); 3377 values[UIProvider.FOLDER_UNREAD_COUNT_COLUMN] = starredCount; 3378 // Add the icon 3379 values[UIProvider.FOLDER_ICON_RES_ID_COLUMN] = R.drawable.ic_folder_star; 3380 break; 3381 } 3382 } 3383 3384 return values; 3385 } 3386 3387 private Cursor uiAccounts(String[] uiProjection) { 3388 final Context context = getContext(); 3389 final SQLiteDatabase db = getDatabase(context); 3390 final Cursor accountIdCursor = 3391 db.rawQuery("select _id from " + Account.TABLE_NAME, new String[0]); 3392 final MatrixCursor mc; 3393 try { 3394 boolean combinedAccount = false; 3395 if (accountIdCursor.getCount() > 1) { 3396 combinedAccount = true; 3397 } 3398 final Bundle extras = new Bundle(); 3399 // Email always returns the accurate number of accounts 3400 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, 1); 3401 mc = new MatrixCursorWithExtra(uiProjection, accountIdCursor.getCount(), extras); 3402 final Object[] values = new Object[uiProjection.length]; 3403 while (accountIdCursor.moveToNext()) { 3404 final String id = accountIdCursor.getString(0); 3405 final Cursor accountCursor = 3406 db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 3407 try { 3408 if (accountCursor.moveToNext()) { 3409 for (int i = 0; i < uiProjection.length; i++) { 3410 values[i] = accountCursor.getString(i); 3411 } 3412 mc.addRow(values); 3413 } 3414 } finally { 3415 accountCursor.close(); 3416 } 3417 } 3418 if (combinedAccount) { 3419 addCombinedAccountRow(mc); 3420 } 3421 } finally { 3422 accountIdCursor.close(); 3423 } 3424 mc.setNotificationUri(context.getContentResolver(), UIPROVIDER_ALL_ACCOUNTS_NOTIFIER); 3425 3426 return mc; 3427 } 3428 3429 private Cursor uiQuickResponseAccount(String[] uiProjection, String account) { 3430 final Context context = getContext(); 3431 final SQLiteDatabase db = getDatabase(context); 3432 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3433 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3434 sb.append(" WHERE " + QuickResponse.ACCOUNT_KEY + "=?"); 3435 final String query = sb.toString(); 3436 return db.rawQuery(query, new String[] {account}); 3437 } 3438 3439 private Cursor uiQuickResponseId(String[] uiProjection, String id) { 3440 final Context context = getContext(); 3441 final SQLiteDatabase db = getDatabase(context); 3442 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3443 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3444 sb.append(" WHERE " + QuickResponse.ID + "=?"); 3445 final String query = sb.toString(); 3446 return db.rawQuery(query, new String[] {id}); 3447 } 3448 3449 private Cursor uiQuickResponse(String[] uiProjection) { 3450 final Context context = getContext(); 3451 final SQLiteDatabase db = getDatabase(context); 3452 final StringBuilder sb = genSelect(getQuickResponseMap(), uiProjection); 3453 sb.append(" FROM " + QuickResponse.TABLE_NAME); 3454 final String query = sb.toString(); 3455 return db.rawQuery(query, new String[0]); 3456 } 3457 3458 /** 3459 * Generate the "attachment list" SQLite query, given a projection from UnifiedEmail 3460 * 3461 * @param uiProjection as passed from UnifiedEmail 3462 * @param contentTypeQueryParameters list of mimeTypes, used as a filter for the attachments 3463 * or null if there are no query parameters 3464 * @return the SQLite query to be executed on the EmailProvider database 3465 */ 3466 private static String genQueryAttachments(String[] uiProjection, 3467 List<String> contentTypeQueryParameters) { 3468 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENT 3469 ContentValues values = new ContentValues(1); 3470 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 3471 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 3472 sb.append(" FROM ") 3473 .append(Attachment.TABLE_NAME) 3474 .append(" WHERE ") 3475 .append(AttachmentColumns.MESSAGE_KEY) 3476 .append(" =? "); 3477 3478 // Filter for certain content types. 3479 // The filter works by adding LIKE operators for each 3480 // content type you wish to request. Content types 3481 // are filtered by performing a case-insensitive "starts with" 3482 // filter. IE, "image/" would return "image/png" as well as "image/jpeg". 3483 if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) { 3484 final int size = contentTypeQueryParameters.size(); 3485 sb.append("AND ("); 3486 for (int i = 0; i < size; i++) { 3487 final String contentType = contentTypeQueryParameters.get(i); 3488 sb.append(AttachmentColumns.MIME_TYPE) 3489 .append(" LIKE '") 3490 .append(contentType) 3491 .append("%'"); 3492 3493 if (i != size - 1) { 3494 sb.append(" OR "); 3495 } 3496 } 3497 sb.append(")"); 3498 } 3499 return sb.toString(); 3500 } 3501 3502 /** 3503 * Generate the "single attachment" SQLite query, given a projection from UnifiedEmail 3504 * 3505 * @param uiProjection as passed from UnifiedEmail 3506 * @return the SQLite query to be executed on the EmailProvider database 3507 */ 3508 private String genQueryAttachment(String[] uiProjection, String idString) { 3509 Long id = Long.parseLong(idString); 3510 Attachment att = Attachment.restoreAttachmentWithId(getContext(), id); 3511 // MAKE SURE THESE VALUES STAY IN SYNC WITH GEN QUERY ATTACHMENTS 3512 ContentValues values = new ContentValues(2); 3513 values.put(AttachmentColumns.CONTENT_URI, 3514 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString()); 3515 values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, 1); 3516 StringBuilder sb = genSelect(getAttachmentMap(), uiProjection, values); 3517 sb.append(" FROM ") 3518 .append(Attachment.TABLE_NAME) 3519 .append(" WHERE ") 3520 .append(AttachmentColumns.ID) 3521 .append(" =? "); 3522 return sb.toString(); 3523 } 3524 3525 /** 3526 * Generate the "subfolder list" SQLite query, given a projection from UnifiedEmail 3527 * 3528 * @param uiProjection as passed from UnifiedEmail 3529 * @return the SQLite query to be executed on the EmailProvider database 3530 */ 3531 private static String genQuerySubfolders(String[] uiProjection) { 3532 StringBuilder sb = genSelect(getFolderListMap(), uiProjection); 3533 sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + MailboxColumns.PARENT_KEY + 3534 " =? ORDER BY "); 3535 sb.append(MAILBOX_ORDER_BY); 3536 return sb.toString(); 3537 } 3538 3539 private static final String COMBINED_ACCOUNT_ID_STRING = Long.toString(COMBINED_ACCOUNT_ID); 3540 3541 /** 3542 * Returns a cursor over all the folders for a specific URI which corresponds to a single 3543 * account. 3544 * @param uri uri to query 3545 * @param uiProjection projection 3546 * @return query result cursor 3547 */ 3548 private Cursor uiFolders(final Uri uri, final String[] uiProjection) { 3549 final Context context = getContext(); 3550 final SQLiteDatabase db = getDatabase(context); 3551 final String id = uri.getPathSegments().get(1); 3552 3553 final Uri notifyUri = 3554 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 3555 3556 final Cursor vc = uiVirtualMailboxes(id, uiProjection); 3557 vc.setNotificationUri(context.getContentResolver(), notifyUri); 3558 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3559 return vc; 3560 } else { 3561 Cursor c = db.rawQuery(genQueryAccountMailboxes(UIProvider.FOLDERS_PROJECTION), 3562 new String[] {id}); 3563 c = getFolderListCursor(c, Long.valueOf(id), uiProjection); 3564 c.setNotificationUri(context.getContentResolver(), notifyUri); 3565 Cursor[] cursors = new Cursor[] {vc, c}; 3566 return new MergeCursor(cursors); 3567 } 3568 } 3569 3570 private Cursor uiVirtualMailboxes(final String id, final String[] uiProjection) { 3571 final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); 3572 3573 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3574 mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX)); 3575 mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_STARRED)); 3576 mc.addRow(getVirtualMailboxRow(COMBINED_ACCOUNT_ID, Mailbox.TYPE_UNREAD)); 3577 } else { 3578 final long acctId = Long.parseLong(id); 3579 mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_STARRED)); 3580 mc.addRow(getVirtualMailboxRow(acctId, Mailbox.TYPE_UNREAD)); 3581 } 3582 3583 return mc; 3584 } 3585 3586 /** 3587 * Returns an array of the default recent folders for a given URI which is unique for an 3588 * account. Some accounts might not have default recent folders, in which case an empty array 3589 * is returned. 3590 * @param id account id 3591 * @return array of URIs 3592 */ 3593 private Uri[] defaultRecentFolders(final String id) { 3594 final SQLiteDatabase db = getDatabase(getContext()); 3595 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 3596 // We don't have default recents for the combined view. 3597 return new Uri[0]; 3598 } 3599 // We search for the types we want, and find corresponding IDs. 3600 final String[] idAndType = { BaseColumns._ID, UIProvider.FolderColumns.TYPE }; 3601 3602 // Sent, Drafts, and Starred are the default recents. 3603 final StringBuilder sb = genSelect(getFolderListMap(), idAndType); 3604 sb.append(" FROM ") 3605 .append(Mailbox.TABLE_NAME) 3606 .append(" WHERE ") 3607 .append(MailboxColumns.ACCOUNT_KEY) 3608 .append(" = ") 3609 .append(id) 3610 .append(" AND ") 3611 .append(MailboxColumns.TYPE) 3612 .append(" IN (") 3613 .append(Mailbox.TYPE_SENT) 3614 .append(", ") 3615 .append(Mailbox.TYPE_DRAFTS) 3616 .append(", ") 3617 .append(Mailbox.TYPE_STARRED) 3618 .append(")"); 3619 LogUtils.d(TAG, "defaultRecentFolders: Query is %s", sb); 3620 final Cursor c = db.rawQuery(sb.toString(), null); 3621 if (c == null || c.getCount() <= 0 || !c.moveToFirst()) { 3622 return new Uri[0]; 3623 } 3624 // Read all the IDs of the mailboxes, and turn them into URIs. 3625 final Uri[] recentFolders = new Uri[c.getCount()]; 3626 int i = 0; 3627 do { 3628 final long folderId = c.getLong(0); 3629 recentFolders[i] = uiUri("uifolder", folderId); 3630 LogUtils.d(TAG, "Default recent folder: %d, with uri %s", folderId, recentFolders[i]); 3631 ++i; 3632 } while (c.moveToNext()); 3633 return recentFolders; 3634 } 3635 3636 /** 3637 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so 3638 * any pending notifications for the corresponding mailbox should be canceled). We also handle 3639 * getExtras() to provide a snapshot of the mailbox's status 3640 */ 3641 static class EmailConversationCursor extends CursorWrapper { 3642 private final long mMailboxId; 3643 private final Context mContext; 3644 private final FolderList mFolderList; 3645 private final Bundle mExtras = new Bundle(); 3646 3647 /** 3648 * When showing a folder, if it's been at least this long since the last sync, 3649 * force a folder refresh. 3650 */ 3651 private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS; 3652 3653 public EmailConversationCursor(final Context context, final Cursor cursor, 3654 final Folder folder, final long mailboxId) { 3655 super(cursor); 3656 mMailboxId = mailboxId; 3657 mContext = context; 3658 mFolderList = FolderList.copyOf(Lists.newArrayList(folder)); 3659 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 3660 3661 if (mailbox != null) { 3662 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3663 mailbox.mUiLastSyncResult); 3664 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount); 3665 if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_BACKGROUND 3666 || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_USER 3667 || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_LIVE) { 3668 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3669 UIProvider.CursorStatus.LOADING); 3670 } else if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_NONE) { 3671 if (mailbox.mSyncInterval == 0 && Mailbox.isSyncableType(mailbox.mType) && 3672 !TextUtils.isEmpty(mailbox.mServerId) && 3673 // TODO: There's potentially a race condition here. 3674 // Consider merging this check with the auto-sync code in respond. 3675 System.currentTimeMillis() - mailbox.mSyncTime 3676 > AUTO_REFRESH_INTERVAL_MS) { 3677 // This will be syncing momentarily 3678 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3679 UIProvider.CursorStatus.LOADING); 3680 } else { 3681 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3682 UIProvider.CursorStatus.COMPLETE); 3683 } 3684 } else { 3685 LogUtils.d(Logging.LOG_TAG, 3686 "Unknown mailbox sync status" + mailbox.mUiSyncStatus); 3687 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3688 UIProvider.CursorStatus.COMPLETE); 3689 } 3690 } else { 3691 // TODO for virtual mailboxes, we may want to do something besides just fake it 3692 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_ERROR, 3693 UIProvider.LastSyncResult.SUCCESS); 3694 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, 3695 cursor != null ? cursor.getCount() : 0); 3696 mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS, 3697 UIProvider.CursorStatus.COMPLETE); 3698 } 3699 } 3700 3701 @Override 3702 public Bundle getExtras() { 3703 return mExtras; 3704 } 3705 3706 @Override 3707 public Bundle respond(Bundle params) { 3708 final String setVisibilityKey = 3709 UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY; 3710 if (params.containsKey(setVisibilityKey)) { 3711 final boolean visible = params.getBoolean(setVisibilityKey); 3712 if (visible) { 3713 // Mark all messages as seen 3714 final ContentResolver resolver = mContext.getContentResolver(); 3715 final ContentValues contentValues = new ContentValues(1); 3716 contentValues.put(MessageColumns.FLAG_SEEN, true); 3717 final Uri uri = EmailContent.Message.CONTENT_URI; 3718 resolver.update(uri, contentValues, MessageColumns.MAILBOX_KEY + " = ?", 3719 new String[] {String.valueOf(mMailboxId)}); 3720 if (params.containsKey( 3721 UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) { 3722 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 3723 if (mailbox != null) { 3724 // For non-push mailboxes, if it's stale (i.e. last sync was a while 3725 // ago), force a sync. 3726 // TODO: Fix the check for whether we're non-push? Right now it checks 3727 // whether we are participating in account sync rules. 3728 if (mailbox.mSyncInterval == 0) { 3729 final long timeSinceLastSync = 3730 System.currentTimeMillis() - mailbox.mSyncTime; 3731 if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) { 3732 final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI + 3733 "/" + QUERY_UIREFRESH + "/" + mailbox.mId); 3734 resolver.query(refreshUri, null, null, null, null); 3735 } 3736 } 3737 } 3738 } 3739 } 3740 } 3741 // Return success 3742 final Bundle response = new Bundle(2); 3743 3744 response.putString(setVisibilityKey, 3745 UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK); 3746 3747 final String rawFoldersKey = 3748 UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS; 3749 if (params.containsKey(rawFoldersKey)) { 3750 response.putParcelable(rawFoldersKey, mFolderList); 3751 } 3752 3753 final String convInfoKey = 3754 UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO; 3755 if (params.containsKey(convInfoKey)) { 3756 response.putParcelable(convInfoKey, generateConversationInfo()); 3757 } 3758 3759 return response; 3760 } 3761 3762 private ConversationInfo generateConversationInfo() { 3763 final int numMessages = getInt(getColumnIndex(ConversationColumns.NUM_MESSAGES)); 3764 final ConversationInfo conversationInfo = new ConversationInfo(numMessages); 3765 3766 conversationInfo.firstSnippet = getString(getColumnIndex(ConversationColumns.SNIPPET)); 3767 3768 final boolean isRead = getInt(getColumnIndex(ConversationColumns.READ)) != 0; 3769 final boolean isStarred = getInt(getColumnIndex(ConversationColumns.STARRED)) != 0; 3770 final String senderString = getString(getColumnIndex(MessageColumns.DISPLAY_NAME)); 3771 3772 final String fromString = getString(getColumnIndex(MessageColumns.FROM_LIST)); 3773 final String email; 3774 3775 if (fromString != null) { 3776 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(fromString); 3777 if (tokens.length > 0) { 3778 email = tokens[0].getAddress(); 3779 } else { 3780 LogUtils.d(TAG, "Couldn't parse email address"); 3781 email = fromString; 3782 } 3783 } else { 3784 email = null; 3785 } 3786 3787 final MessageInfo messageInfo = new MessageInfo(isRead, isStarred, senderString, 3788 0 /* priority */, email); 3789 conversationInfo.addMessage(messageInfo); 3790 3791 return conversationInfo; 3792 } 3793 } 3794 3795 /** 3796 * Convenience method to create a {@link Folder} 3797 * @param context to get a {@link ContentResolver} 3798 * @param mailboxId id of the {@link Mailbox} that we want 3799 * @return the {@link Folder} or null 3800 */ 3801 public static Folder getFolder(Context context, long mailboxId) { 3802 final ContentResolver resolver = context.getContentResolver(); 3803 final Cursor fc = resolver.query(EmailProvider.uiUri("uifolder", mailboxId), 3804 UIProvider.FOLDERS_PROJECTION, null, null, null); 3805 3806 if (fc == null) { 3807 LogUtils.e(TAG, "Null folder cursor for mailboxId %d", mailboxId); 3808 return null; 3809 } 3810 3811 Folder uiFolder = null; 3812 try { 3813 if (fc.moveToFirst()) { 3814 uiFolder = new Folder(fc); 3815 } 3816 } finally { 3817 fc.close(); 3818 } 3819 return uiFolder; 3820 } 3821 3822 static class AttachmentsCursor extends CursorWrapper { 3823 private final int mContentUriIndex; 3824 private final int mUriIndex; 3825 private final Context mContext; 3826 3827 public AttachmentsCursor(Context context, Cursor cursor) { 3828 super(cursor); 3829 mContentUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI); 3830 mUriIndex = cursor.getColumnIndex(UIProvider.AttachmentColumns.URI); 3831 mContext = context; 3832 } 3833 3834 @Override 3835 public String getString(int column) { 3836 if (column == mContentUriIndex) { 3837 final Uri uri = Uri.parse(getString(mUriIndex)); 3838 final long id = Long.parseLong(uri.getLastPathSegment()); 3839 final Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 3840 if (att == null) return ""; 3841 if (!TextUtils.isEmpty(att.getCachedFileUri())) { 3842 return att.getCachedFileUri(); 3843 } 3844 3845 final String contentUri; 3846 // Until the package installer can handle opening apks from a content:// uri, for 3847 // any apk that was successfully saved in external storage, return the 3848 // content uri from the attachment 3849 if (att.mUiDestination == UIProvider.AttachmentDestination.EXTERNAL && 3850 att.mUiState == UIProvider.AttachmentState.SAVED && 3851 TextUtils.equals(att.mMimeType, MimeType.ANDROID_ARCHIVE)) { 3852 contentUri = att.getContentUri(); 3853 } else { 3854 contentUri = 3855 AttachmentUtilities.getAttachmentUri(att.mAccountKey, id).toString(); 3856 } 3857 return contentUri; 3858 3859 } else { 3860 return super.getString(column); 3861 } 3862 } 3863 } 3864 3865 /** 3866 * For debugging purposes; shouldn't be used in production code 3867 */ 3868 @SuppressWarnings("unused") 3869 static class CloseDetectingCursor extends CursorWrapper { 3870 3871 public CloseDetectingCursor(Cursor cursor) { 3872 super(cursor); 3873 } 3874 3875 @Override 3876 public void close() { 3877 super.close(); 3878 LogUtils.d(TAG, "Closing cursor", new Error()); 3879 } 3880 } 3881 3882 /** 3883 * Converts a mailbox in a row of the mailboxCursor into a row 3884 * in the supplied {@link MatrixCursor} in the format required for {@link Folder}. 3885 * As a convenience, the modified {@link MatrixCursor} is also returned. 3886 * @param mc the {@link MatrixCursor} into which the mailbox data will be converted 3887 * @param projectionLength the length of the projection for this Cursor 3888 * @param mailboxCursor the cursor supplying the mailbox data 3889 * @param nameColumn column in the cursor containing the folder name value 3890 * @param typeColumn column in the cursor containing the folder type value 3891 * @return the {@link MatrixCursor} containing the transformed data. 3892 */ 3893 private Cursor getUiFolderCursorRowFromMailboxCursorRow( 3894 MatrixCursor mc, int projectionLength, Cursor mailboxCursor, 3895 int nameColumn, int typeColumn) { 3896 final MatrixCursor.RowBuilder builder = mc.newRow(); 3897 for (int i = 0; i < projectionLength; i++) { 3898 // If we are at the name column, get the type 3899 // and use it to use a properly translated string 3900 // from resources instead of the display name. 3901 // This ignores display names for system mailboxes. 3902 if (nameColumn == i) { 3903 // We implicitly assume that if name is requested, 3904 // type has also been requested. If not, this will 3905 // error in unknown ways. 3906 final int type = mailboxCursor.getInt(typeColumn); 3907 builder.add(getFolderDisplayName(type, mailboxCursor.getString(i))); 3908 } else { 3909 builder.add(mailboxCursor.getString(i)); 3910 } 3911 } 3912 return mc; 3913 } 3914 3915 /** 3916 * Takes a uifolder cursor (that was generated with a full projection) and remaps values for 3917 * columns that are difficult to generate in the SQL query. This currently includes: 3918 * - Folder name (due to system folder localization). 3919 * - Capabilities (due to this varying by account protocol). 3920 * - Persistent id (due to needing to base64 encode it). 3921 * - Load more uri (due to this varying by account protocol). 3922 * TODO: This would be better as a CursorWrapper, rather than doing a copy. 3923 * @param inputCursor A cursor containing all columns of {@link UIProvider.FolderColumns}. 3924 * Strictly speaking doesn't need all, but simpler if we assume that. 3925 * @param outputCursor A MatrixCursor which this function will populate. 3926 * @param accountId The account id for the mailboxes in this query. 3927 * @param uiProjection The projection specified by the query. 3928 */ 3929 private void remapFolderCursor(final Cursor inputCursor, final MatrixCursor outputCursor, 3930 final long accountId, final String[] uiProjection) { 3931 // Return early if our input cursor is empty. 3932 if (inputCursor == null || inputCursor.getCount() == 0) { 3933 return; 3934 } 3935 // Get the column indices for the columns we need during remapping. 3936 // While we currently could assume the column indicies for UIProvider.FOLDERS_PROJECTION 3937 // and therefore avoid the calls to getColumnIndex, this at least tries to future-proof a 3938 // bit. 3939 // Note that id and type MUST be present for this function to work correctly. 3940 final int idColumn = inputCursor.getColumnIndex(BaseColumns._ID); 3941 final int typeColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.TYPE); 3942 final int nameColumn = inputCursor.getColumnIndex(UIProvider.FolderColumns.NAME); 3943 final int capabilitiesColumn = 3944 inputCursor.getColumnIndex(UIProvider.FolderColumns.CAPABILITIES); 3945 final int persistentIdColumn = 3946 inputCursor.getColumnIndex(UIProvider.FolderColumns.PERSISTENT_ID); 3947 final int loadMoreUriColumn = 3948 inputCursor.getColumnIndex(UIProvider.FolderColumns.LOAD_MORE_URI); 3949 3950 // Get the EmailServiceInfo for the current account. 3951 final Context context = getContext(); 3952 final String protocol = Account.getProtocol(context, accountId); 3953 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 3954 3955 // Build the return cursor. We iterate over all rows of the input cursor and construct 3956 // a row in the output using the columns in uiProjection. 3957 while (inputCursor.moveToNext()) { 3958 final MatrixCursor.RowBuilder builder = outputCursor.newRow(); 3959 final int type = inputCursor.getInt(typeColumn); 3960 for (int i = 0; i < uiProjection.length; i++) { 3961 // Find the index in the input cursor corresponding the column requested in the 3962 // output projection. 3963 final int index = inputCursor.getColumnIndex(uiProjection[i]); 3964 if (index == -1) { 3965 // We don't have this value, so put a blank in the output and move on. 3966 builder.add(null); 3967 continue; 3968 } 3969 final String value = inputCursor.getString(index); 3970 // remapped indicates whether we've written a value to the output for this column. 3971 final boolean remapped; 3972 if (nameColumn == index) { 3973 // Remap folder name for system folders. 3974 builder.add(getFolderDisplayName(type, value)); 3975 remapped = true; 3976 } else if (capabilitiesColumn == index) { 3977 // Get the correct capabilities for this folder. 3978 builder.add(getFolderCapabilities(info, type, inputCursor.getLong(idColumn))); 3979 remapped = true; 3980 } else if (persistentIdColumn == index) { 3981 // Hash the persistent id. 3982 builder.add(Base64.encodeToString(value.getBytes(), 3983 Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); 3984 remapped = true; 3985 } else if (loadMoreUriColumn == index && type != Mailbox.TYPE_SEARCH && 3986 (info == null || !info.offerLoadMore)) { 3987 // Blank the load more uri for account types that don't offer it. 3988 // Note that all account types permit load more for search results. 3989 builder.add(null); 3990 remapped = true; 3991 } else { 3992 remapped = false; 3993 } 3994 // If the above logic didn't write some other value to the output, use the value 3995 // from the input cursor. 3996 if (!remapped) { 3997 builder.add(value); 3998 } 3999 } 4000 } 4001 } 4002 4003 private Cursor getFolderListCursor(final Cursor inputCursor, final long accountId, 4004 final String[] uiProjection) { 4005 final MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection); 4006 if (inputCursor != null) { 4007 try { 4008 remapFolderCursor(inputCursor, mc, accountId, uiProjection); 4009 } finally { 4010 inputCursor.close(); 4011 } 4012 } 4013 return mc; 4014 } 4015 4016 /** 4017 * Returns a {@link String} from Resources corresponding 4018 * to the {@link UIProvider.FolderType} requested. 4019 * @param folderType {@link UIProvider.FolderType} value for the folder 4020 * @param defaultName a {@link String} to use in case the {@link UIProvider.FolderType} 4021 * provided is not a system folder. 4022 * @return a {@link String} to use as the display name for the folder 4023 */ 4024 private String getFolderDisplayName(int folderType, String defaultName) { 4025 final int resId; 4026 switch (folderType) { 4027 case UIProvider.FolderType.INBOX: 4028 resId = R.string.mailbox_name_display_inbox; 4029 break; 4030 case UIProvider.FolderType.OUTBOX: 4031 resId = R.string.mailbox_name_display_outbox; 4032 break; 4033 case UIProvider.FolderType.DRAFT: 4034 resId = R.string.mailbox_name_display_drafts; 4035 break; 4036 case UIProvider.FolderType.TRASH: 4037 resId = R.string.mailbox_name_display_trash; 4038 break; 4039 case UIProvider.FolderType.SENT: 4040 resId = R.string.mailbox_name_display_sent; 4041 break; 4042 case UIProvider.FolderType.SPAM: 4043 resId = R.string.mailbox_name_display_junk; 4044 break; 4045 case UIProvider.FolderType.STARRED: 4046 resId = R.string.mailbox_name_display_starred; 4047 break; 4048 case UIProvider.FolderType.UNREAD: 4049 resId = R.string.mailbox_name_display_unread; 4050 break; 4051 default: 4052 return defaultName; 4053 } 4054 return getContext().getString(resId); 4055 } 4056 4057 /** 4058 * Converts a {@link Mailbox} type value to its {@link UIProvider.FolderType} 4059 * equivalent. 4060 * @param mailboxType a {@link Mailbox} type 4061 * @return a {@link UIProvider.FolderType} value 4062 */ 4063 private static int getFolderTypeFromMailboxType(int mailboxType) { 4064 switch (mailboxType) { 4065 case Mailbox.TYPE_INBOX: 4066 return UIProvider.FolderType.INBOX; 4067 case Mailbox.TYPE_OUTBOX: 4068 return UIProvider.FolderType.OUTBOX; 4069 case Mailbox.TYPE_DRAFTS: 4070 return UIProvider.FolderType.DRAFT; 4071 case Mailbox.TYPE_TRASH: 4072 return UIProvider.FolderType.TRASH; 4073 case Mailbox.TYPE_SENT: 4074 return UIProvider.FolderType.SENT; 4075 case Mailbox.TYPE_JUNK: 4076 return UIProvider.FolderType.SPAM; 4077 case Mailbox.TYPE_STARRED: 4078 return UIProvider.FolderType.STARRED; 4079 case Mailbox.TYPE_UNREAD: 4080 return UIProvider.FolderType.UNREAD; 4081 case Mailbox.TYPE_SEARCH: 4082 // TODO Can the DEFAULT type be removed from SEARCH folders? 4083 return UIProvider.FolderType.DEFAULT | UIProvider.FolderType.SEARCH; 4084 default: 4085 return UIProvider.FolderType.DEFAULT; 4086 } 4087 } 4088 4089 /** 4090 * Handle UnifiedEmail queries here (dispatched from query()) 4091 * 4092 * @param match the UriMatcher match for the original uri passed in from UnifiedEmail 4093 * @param uri the original uri passed in from UnifiedEmail 4094 * @param uiProjection the projection passed in from UnifiedEmail 4095 * @param unseenOnly <code>true</code> to only return unseen messages (where supported) 4096 * @return the result Cursor 4097 */ 4098 private Cursor uiQuery(int match, Uri uri, String[] uiProjection, final boolean unseenOnly) { 4099 Context context = getContext(); 4100 ContentResolver resolver = context.getContentResolver(); 4101 SQLiteDatabase db = getDatabase(context); 4102 // Should we ever return null, or throw an exception?? 4103 Cursor c = null; 4104 String id = uri.getPathSegments().get(1); 4105 Uri notifyUri = null; 4106 switch(match) { 4107 case UI_ALL_FOLDERS: 4108 notifyUri = 4109 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 4110 final Cursor vc = uiVirtualMailboxes(id, uiProjection); 4111 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 4112 // There's no real mailboxes, so just return the virtual ones 4113 c = vc; 4114 } else { 4115 // Return real and virtual mailboxes alike 4116 final Cursor rawc = db.rawQuery(genQueryAccountAllMailboxes(uiProjection), 4117 new String[] {id}); 4118 rawc.setNotificationUri(context.getContentResolver(), notifyUri); 4119 vc.setNotificationUri(context.getContentResolver(), notifyUri); 4120 c = new MergeCursor(new Cursor[] {rawc, vc}); 4121 } 4122 break; 4123 case UI_FULL_FOLDERS: 4124 c = db.rawQuery(genQueryAccountAllMailboxes(UIProvider.FOLDERS_PROJECTION), 4125 new String[] {id}); 4126 c = getFolderListCursor(c, Long.valueOf(id), uiProjection); 4127 notifyUri = 4128 UIPROVIDER_FOLDERLIST_NOTIFIER.buildUpon().appendEncodedPath(id).build(); 4129 break; 4130 case UI_RECENT_FOLDERS: 4131 c = db.rawQuery(genQueryRecentMailboxes(uiProjection), new String[] {id}); 4132 notifyUri = UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 4133 break; 4134 case UI_SUBFOLDERS: 4135 c = db.rawQuery(genQuerySubfolders(UIProvider.FOLDERS_PROJECTION), 4136 new String[] {id}); 4137 c = getFolderListCursor(c, Mailbox.getAccountIdForMailbox(context, id), 4138 uiProjection); 4139 // Get notifications for any folder changes on this account. This is broader than 4140 // we need but otherwise we'd need for every folder change to notify on all relevant 4141 // subtrees. For now we opt for simplicity. 4142 final long accountId = Mailbox.getAccountIdForMailbox(context, id); 4143 notifyUri = ContentUris.withAppendedId(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 4144 break; 4145 case UI_MESSAGES: 4146 long mailboxId = Long.parseLong(id); 4147 final Folder folder = getFolder(context, mailboxId); 4148 if (folder == null) { 4149 // This mailboxId is bogus. Return an empty cursor 4150 // TODO: Make callers of this query handle null cursors instead b/10819309 4151 return new MatrixCursor(uiProjection); 4152 } 4153 if (isVirtualMailbox(mailboxId)) { 4154 c = getVirtualMailboxMessagesCursor(db, uiProjection, mailboxId, unseenOnly); 4155 } else { 4156 c = db.rawQuery( 4157 genQueryMailboxMessages(uiProjection, unseenOnly), new String[] {id}); 4158 } 4159 notifyUri = UIPROVIDER_CONVERSATION_NOTIFIER.buildUpon().appendPath(id).build(); 4160 c = new EmailConversationCursor(context, c, folder, mailboxId); 4161 break; 4162 case UI_MESSAGE: 4163 MessageQuery qq = genQueryViewMessage(uiProjection, id); 4164 String sql = qq.query; 4165 String attJson = qq.attachmentJson; 4166 // With attachments, we have another argument to bind 4167 if (attJson != null) { 4168 c = db.rawQuery(sql, new String[] {attJson, id}); 4169 } else { 4170 c = db.rawQuery(sql, new String[] {id}); 4171 } 4172 notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build(); 4173 break; 4174 case UI_ATTACHMENTS: 4175 final List<String> contentTypeQueryParameters = 4176 uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE); 4177 c = db.rawQuery(genQueryAttachments(uiProjection, contentTypeQueryParameters), 4178 new String[] {id}); 4179 c = new AttachmentsCursor(context, c); 4180 notifyUri = UIPROVIDER_ATTACHMENTS_NOTIFIER.buildUpon().appendPath(id).build(); 4181 break; 4182 case UI_ATTACHMENT: 4183 c = db.rawQuery(genQueryAttachment(uiProjection, id), new String[] {id}); 4184 notifyUri = UIPROVIDER_ATTACHMENT_NOTIFIER.buildUpon().appendPath(id).build(); 4185 break; 4186 case UI_FOLDER: 4187 mailboxId = Long.parseLong(id); 4188 if (isVirtualMailbox(mailboxId)) { 4189 c = getVirtualMailboxCursor(mailboxId); 4190 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 4191 } else { 4192 c = db.rawQuery(genQueryMailbox(uiProjection, id), new String[]{id}); 4193 final List<String> projectionList = Arrays.asList(uiProjection); 4194 final int nameColumn = projectionList.indexOf(UIProvider.FolderColumns.NAME); 4195 final int typeColumn = projectionList.indexOf(UIProvider.FolderColumns.TYPE); 4196 if (c.moveToFirst()) { 4197 c = getUiFolderCursorRowFromMailboxCursorRow( 4198 new MatrixCursorWithCachedColumns(uiProjection), 4199 uiProjection.length, c, nameColumn, typeColumn); 4200 } 4201 notifyUri = UIPROVIDER_FOLDER_NOTIFIER.buildUpon().appendPath(id).build(); 4202 } 4203 break; 4204 case UI_ACCOUNT: 4205 if (id.equals(COMBINED_ACCOUNT_ID_STRING)) { 4206 MatrixCursor mc = new MatrixCursorWithCachedColumns(uiProjection, 1); 4207 addCombinedAccountRow(mc); 4208 c = mc; 4209 } else { 4210 c = db.rawQuery(genQueryAccount(uiProjection, id), new String[] {id}); 4211 } 4212 notifyUri = UIPROVIDER_ACCOUNT_NOTIFIER.buildUpon().appendPath(id).build(); 4213 break; 4214 case UI_CONVERSATION: 4215 c = db.rawQuery(genQueryConversation(uiProjection), new String[] {id}); 4216 break; 4217 } 4218 if (notifyUri != null) { 4219 c.setNotificationUri(resolver, notifyUri); 4220 } 4221 return c; 4222 } 4223 4224 /** 4225 * Convert a UIProvider attachment to an EmailProvider attachment (for sending); we only need 4226 * a few of the fields 4227 * @param uiAtt the UIProvider attachment to convert 4228 * @param cachedFile the path to the cached file to 4229 * @return the EmailProvider attachment 4230 */ 4231 // TODO(pwestbro): once the Attachment contains the cached uri, the second parameter can be 4232 // removed 4233 // TODO(mhibdon): if the UI Attachment contained the account key, the third parameter could 4234 // be removed. 4235 private static Attachment convertUiAttachmentToAttachment( 4236 com.android.mail.providers.Attachment uiAtt, String cachedFile, long accountKey) { 4237 final Attachment att = new Attachment(); 4238 4239 att.setContentUri(uiAtt.contentUri.toString()); 4240 4241 if (!TextUtils.isEmpty(cachedFile)) { 4242 // Generate the content provider uri for this cached file 4243 final Uri.Builder cachedFileBuilder = Uri.parse( 4244 "content://" + EmailContent.AUTHORITY + "/attachment/cachedFile").buildUpon(); 4245 cachedFileBuilder.appendQueryParameter(Attachment.CACHED_FILE_QUERY_PARAM, cachedFile); 4246 att.setCachedFileUri(cachedFileBuilder.build().toString()); 4247 } 4248 att.mAccountKey = accountKey; 4249 att.mFileName = uiAtt.getName(); 4250 att.mMimeType = uiAtt.getContentType(); 4251 att.mSize = uiAtt.size; 4252 return att; 4253 } 4254 4255 /** 4256 * Create a mailbox given the account and mailboxType. 4257 */ 4258 private Mailbox createMailbox(long accountId, int mailboxType) { 4259 Context context = getContext(); 4260 Mailbox box = Mailbox.newSystemMailbox(context, accountId, mailboxType); 4261 // Make sure drafts and save will show up in recents... 4262 // If these already exist (from old Email app), they will have touch times 4263 switch (mailboxType) { 4264 case Mailbox.TYPE_DRAFTS: 4265 box.mLastTouchedTime = Mailbox.DRAFTS_DEFAULT_TOUCH_TIME; 4266 break; 4267 case Mailbox.TYPE_SENT: 4268 box.mLastTouchedTime = Mailbox.SENT_DEFAULT_TOUCH_TIME; 4269 break; 4270 } 4271 box.save(context); 4272 return box; 4273 } 4274 4275 /** 4276 * Given an account name and a mailbox type, return that mailbox, creating it if necessary 4277 * @param accountId the account id to use 4278 * @param mailboxType the type of mailbox we're trying to find 4279 * @return the mailbox of the given type for the account in the uri, or null if not found 4280 */ 4281 private Mailbox getMailboxByAccountIdAndType(final long accountId, final int mailboxType) { 4282 Mailbox mailbox = Mailbox.restoreMailboxOfType(getContext(), accountId, mailboxType); 4283 if (mailbox == null) { 4284 mailbox = createMailbox(accountId, mailboxType); 4285 } 4286 return mailbox; 4287 } 4288 4289 /** 4290 * Given a mailbox and the content values for a message, create/save the message in the mailbox 4291 * @param mailbox the mailbox to use 4292 * @param extras the bundle containing the message fields 4293 * @return the uri of the newly created message 4294 * TODO(yph): The following fields are available in extras but unused, verify whether they 4295 * should be respected: 4296 * - UIProvider.MessageColumns.SNIPPET 4297 * - UIProvider.MessageColumns.REPLY_TO 4298 * - UIProvider.MessageColumns.FROM 4299 * - UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS 4300 */ 4301 private Uri uiSaveMessage(Message msg, Mailbox mailbox, Bundle extras) { 4302 final Context context = getContext(); 4303 // Fill in the message 4304 final Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); 4305 if (account == null) return null; 4306 msg.mFrom = account.mEmailAddress; 4307 msg.mTimeStamp = System.currentTimeMillis(); 4308 msg.mTo = extras.getString(UIProvider.MessageColumns.TO); 4309 msg.mCc = extras.getString(UIProvider.MessageColumns.CC); 4310 msg.mBcc = extras.getString(UIProvider.MessageColumns.BCC); 4311 msg.mSubject = extras.getString(UIProvider.MessageColumns.SUBJECT); 4312 msg.mText = extras.getString(UIProvider.MessageColumns.BODY_TEXT); 4313 msg.mHtml = extras.getString(UIProvider.MessageColumns.BODY_HTML); 4314 msg.mMailboxKey = mailbox.mId; 4315 msg.mAccountKey = mailbox.mAccountKey; 4316 msg.mDisplayName = msg.mTo; 4317 msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 4318 msg.mFlagRead = true; 4319 msg.mFlagSeen = true; 4320 final Integer quoteStartPos = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 4321 msg.mQuotedTextStartPos = quoteStartPos == null ? 0 : quoteStartPos; 4322 int flags = 0; 4323 final int draftType = extras.getInt(UIProvider.MessageColumns.DRAFT_TYPE); 4324 switch(draftType) { 4325 case DraftType.FORWARD: 4326 flags |= Message.FLAG_TYPE_FORWARD; 4327 break; 4328 case DraftType.REPLY_ALL: 4329 flags |= Message.FLAG_TYPE_REPLY_ALL; 4330 //$FALL-THROUGH$ 4331 case DraftType.REPLY: 4332 flags |= Message.FLAG_TYPE_REPLY; 4333 break; 4334 case DraftType.COMPOSE: 4335 flags |= Message.FLAG_TYPE_ORIGINAL; 4336 break; 4337 } 4338 int draftInfo = 0; 4339 if (extras.containsKey(UIProvider.MessageColumns.QUOTE_START_POS)) { 4340 draftInfo = extras.getInt(UIProvider.MessageColumns.QUOTE_START_POS); 4341 if (extras.getInt(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT) != 0) { 4342 draftInfo |= Message.DRAFT_INFO_APPEND_REF_MESSAGE; 4343 } 4344 } 4345 if (!extras.containsKey(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT)) { 4346 flags |= Message.FLAG_NOT_INCLUDE_QUOTED_TEXT; 4347 } 4348 msg.mDraftInfo = draftInfo; 4349 msg.mFlags = flags; 4350 4351 final String ref = extras.getString(UIProvider.MessageColumns.REF_MESSAGE_ID); 4352 if (ref != null && msg.mQuotedTextStartPos >= 0) { 4353 String refId = Uri.parse(ref).getLastPathSegment(); 4354 try { 4355 msg.mSourceKey = Long.parseLong(refId); 4356 } catch (NumberFormatException e) { 4357 // This will be zero; the default 4358 } 4359 } 4360 4361 // Get attachments from the ContentValues 4362 final List<com.android.mail.providers.Attachment> uiAtts = 4363 com.android.mail.providers.Attachment.fromJSONArray( 4364 extras.getString(UIProvider.MessageColumns.ATTACHMENTS)); 4365 final ArrayList<Attachment> atts = new ArrayList<Attachment>(); 4366 boolean hasUnloadedAttachments = false; 4367 Bundle attachmentFds = 4368 extras.getParcelable(UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP); 4369 for (com.android.mail.providers.Attachment uiAtt: uiAtts) { 4370 final Uri attUri = uiAtt.uri; 4371 if (attUri != null && attUri.getAuthority().equals(EmailContent.AUTHORITY)) { 4372 // If it's one of ours, retrieve the attachment and add it to the list 4373 final long attId = Long.parseLong(attUri.getLastPathSegment()); 4374 final Attachment att = Attachment.restoreAttachmentWithId(context, attId); 4375 if (att != null) { 4376 // We must clone the attachment into a new one for this message; easiest to 4377 // use a parcel here 4378 final Parcel p = Parcel.obtain(); 4379 att.writeToParcel(p, 0); 4380 p.setDataPosition(0); 4381 final Attachment attClone = new Attachment(p); 4382 p.recycle(); 4383 // Clear the messageKey (this is going to be a new attachment) 4384 attClone.mMessageKey = 0; 4385 // If we're sending this, it's not loaded, and we're not smart forwarding 4386 // add the download flag, so that ADS will start up 4387 if (mailbox.mType == Mailbox.TYPE_OUTBOX && att.getContentUri() == null && 4388 ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) { 4389 attClone.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; 4390 hasUnloadedAttachments = true; 4391 } 4392 atts.add(attClone); 4393 } 4394 } else { 4395 // Cache the attachment. This will allow us to send it, if the permissions are 4396 // revoked. 4397 final String cachedFileUri = 4398 AttachmentUtils.cacheAttachmentUri(context, uiAtt, attachmentFds); 4399 4400 // Convert external attachment to one of ours and add to the list 4401 atts.add(convertUiAttachmentToAttachment(uiAtt, cachedFileUri, msg.mAccountKey)); 4402 } 4403 } 4404 if (!atts.isEmpty()) { 4405 msg.mAttachments = atts; 4406 msg.mFlagAttachment = true; 4407 if (hasUnloadedAttachments) { 4408 Utility.showToast(context, R.string.message_view_attachment_background_load); 4409 } 4410 } 4411 // Save it or update it... 4412 if (!msg.isSaved()) { 4413 msg.save(context); 4414 } else { 4415 // This is tricky due to how messages/attachments are saved; rather than putz with 4416 // what's changed, we'll delete/re-add them 4417 final ArrayList<ContentProviderOperation> ops = 4418 new ArrayList<ContentProviderOperation>(); 4419 // Delete all existing attachments 4420 ops.add(ContentProviderOperation.newDelete( 4421 ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, msg.mId)) 4422 .build()); 4423 // Delete the body 4424 ops.add(ContentProviderOperation.newDelete(Body.CONTENT_URI) 4425 .withSelection(Body.MESSAGE_KEY + "=?", new String[] {Long.toString(msg.mId)}) 4426 .build()); 4427 // Add the ops for the message, atts, and body 4428 msg.addSaveOps(ops); 4429 // Do it! 4430 try { 4431 applyBatch(ops); 4432 } catch (OperationApplicationException e) { 4433 LogUtils.d(TAG, "applyBatch exception"); 4434 } 4435 } 4436 notifyUIMessage(msg.mId); 4437 4438 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 4439 startSync(mailbox, 0); 4440 final long originalMsgId = msg.mSourceKey; 4441 if (originalMsgId != 0) { 4442 final Message originalMsg = Message.restoreMessageWithId(context, originalMsgId); 4443 // If the original message exists, set its forwarded/replied to flags 4444 if (originalMsg != null) { 4445 final ContentValues cv = new ContentValues(); 4446 flags = originalMsg.mFlags; 4447 switch(draftType) { 4448 case DraftType.FORWARD: 4449 flags |= Message.FLAG_FORWARDED; 4450 break; 4451 case DraftType.REPLY_ALL: 4452 case DraftType.REPLY: 4453 flags |= Message.FLAG_REPLIED_TO; 4454 break; 4455 } 4456 cv.put(Message.FLAGS, flags); 4457 context.getContentResolver().update(ContentUris.withAppendedId( 4458 Message.CONTENT_URI, originalMsgId), cv, null, null); 4459 } 4460 } 4461 } 4462 return uiUri("uimessage", msg.mId); 4463 } 4464 4465 private Uri uiSaveDraftMessage(final long accountId, final Bundle extras) { 4466 final Mailbox mailbox = 4467 getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_DRAFTS); 4468 if (mailbox == null) return null; 4469 final Message msg; 4470 if (extras.containsKey(BaseColumns._ID)) { 4471 final long messageId = extras.getLong(BaseColumns._ID); 4472 msg = Message.restoreMessageWithId(getContext(), messageId); 4473 } else { 4474 msg = new Message(); 4475 } 4476 return uiSaveMessage(msg, mailbox, extras); 4477 } 4478 4479 private Uri uiSendDraftMessage(final long accountId, final Bundle extras) { 4480 final Context context = getContext(); 4481 final Message msg; 4482 if (extras.containsKey(BaseColumns._ID)) { 4483 final long messageId = extras.getLong(BaseColumns._ID); 4484 msg = Message.restoreMessageWithId(getContext(), messageId); 4485 } else { 4486 msg = new Message(); 4487 } 4488 4489 if (msg == null) return null; 4490 final Mailbox mailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_OUTBOX); 4491 if (mailbox == null) return null; 4492 // Make sure the sent mailbox exists, since it will be necessary soon. 4493 // TODO(yph): move system mailbox creation to somewhere sane. 4494 final Mailbox sentMailbox = getMailboxByAccountIdAndType(accountId, Mailbox.TYPE_SENT); 4495 if (sentMailbox == null) return null; 4496 final Uri messageUri = uiSaveMessage(msg, mailbox, extras); 4497 // Kick observers 4498 context.getContentResolver().notifyChange(Mailbox.CONTENT_URI, null); 4499 return messageUri; 4500 } 4501 4502 private static void putIntegerLongOrBoolean(ContentValues values, String columnName, 4503 Object value) { 4504 if (value instanceof Integer) { 4505 Integer intValue = (Integer)value; 4506 values.put(columnName, intValue); 4507 } else if (value instanceof Boolean) { 4508 Boolean boolValue = (Boolean)value; 4509 values.put(columnName, boolValue ? 1 : 0); 4510 } else if (value instanceof Long) { 4511 Long longValue = (Long)value; 4512 values.put(columnName, longValue); 4513 } 4514 } 4515 4516 /** 4517 * Update the timestamps for the folders specified and notifies on the recent folder URI. 4518 * @param folders array of folder Uris to update 4519 * @return number of folders updated 4520 */ 4521 private static int updateTimestamp(final Context context, String id, Uri[] folders){ 4522 int updated = 0; 4523 final long now = System.currentTimeMillis(); 4524 final ContentResolver resolver = context.getContentResolver(); 4525 final ContentValues touchValues = new ContentValues(); 4526 for (final Uri folder : folders) { 4527 touchValues.put(MailboxColumns.LAST_TOUCHED_TIME, now); 4528 LogUtils.d(TAG, "updateStamp: %s updated", folder); 4529 updated += resolver.update(folder, touchValues, null, null); 4530 } 4531 final Uri toNotify = 4532 UIPROVIDER_RECENT_FOLDERS_NOTIFIER.buildUpon().appendPath(id).build(); 4533 LogUtils.d(TAG, "updateTimestamp: Notifying on %s", toNotify); 4534 resolver.notifyChange(toNotify, null); 4535 return updated; 4536 } 4537 4538 /** 4539 * Updates the recent folders. The values to be updated are specified as ContentValues pairs 4540 * of (Folder URI, access timestamp). Returns nonzero if successful, always. 4541 * @param uri provider query uri 4542 * @param values uri, timestamp pairs 4543 * @return nonzero value always. 4544 */ 4545 private int uiUpdateRecentFolders(Uri uri, ContentValues values) { 4546 final int numFolders = values.size(); 4547 final String id = uri.getPathSegments().get(1); 4548 final Uri[] folders = new Uri[numFolders]; 4549 final Context context = getContext(); 4550 int i = 0; 4551 for (final String uriString : values.keySet()) { 4552 folders[i] = Uri.parse(uriString); 4553 } 4554 return updateTimestamp(context, id, folders); 4555 } 4556 4557 /** 4558 * Populates the recent folders according to the design. 4559 * @param uri provider query uri 4560 * @return the number of recent folders were populated. 4561 */ 4562 private int uiPopulateRecentFolders(Uri uri) { 4563 final Context context = getContext(); 4564 final String id = uri.getLastPathSegment(); 4565 final Uri[] recentFolders = defaultRecentFolders(id); 4566 final int numFolders = recentFolders.length; 4567 if (numFolders <= 0) { 4568 return 0; 4569 } 4570 final int rowsUpdated = updateTimestamp(context, id, recentFolders); 4571 LogUtils.d(TAG, "uiPopulateRecentFolders: %d folders changed", rowsUpdated); 4572 return rowsUpdated; 4573 } 4574 4575 private int uiUpdateAttachment(Uri uri, ContentValues uiValues) { 4576 int result = 0; 4577 Integer stateValue = uiValues.getAsInteger(UIProvider.AttachmentColumns.STATE); 4578 if (stateValue != null) { 4579 // This is a command from UIProvider 4580 long attachmentId = Long.parseLong(uri.getLastPathSegment()); 4581 Context context = getContext(); 4582 Attachment attachment = 4583 Attachment.restoreAttachmentWithId(context, attachmentId); 4584 if (attachment == null) { 4585 // Went away; ah, well... 4586 return result; 4587 } 4588 int state = stateValue; 4589 ContentValues values = new ContentValues(); 4590 if (state == UIProvider.AttachmentState.NOT_SAVED 4591 || state == UIProvider.AttachmentState.REDOWNLOADING) { 4592 // Set state, try to cancel request 4593 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.NOT_SAVED); 4594 values.put(AttachmentColumns.FLAGS, 4595 attachment.mFlags &= ~Attachment.FLAG_DOWNLOAD_USER_REQUEST); 4596 attachment.update(context, values); 4597 result = 1; 4598 } 4599 if (state == UIProvider.AttachmentState.DOWNLOADING 4600 || state == UIProvider.AttachmentState.REDOWNLOADING) { 4601 // Set state and destination; request download 4602 values.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.DOWNLOADING); 4603 Integer destinationValue = 4604 uiValues.getAsInteger(UIProvider.AttachmentColumns.DESTINATION); 4605 values.put(AttachmentColumns.UI_DESTINATION, 4606 destinationValue == null ? 0 : destinationValue); 4607 values.put(AttachmentColumns.FLAGS, 4608 attachment.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 4609 attachment.update(context, values); 4610 result = 1; 4611 } 4612 if (state == UIProvider.AttachmentState.SAVED) { 4613 // If this is an inline attachment, notify message has changed 4614 if (!TextUtils.isEmpty(attachment.mContentId)) { 4615 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, attachment.mMessageKey); 4616 } 4617 result = 1; 4618 } 4619 } 4620 return result; 4621 } 4622 4623 private int uiUpdateFolder(final Context context, Uri uri, ContentValues uiValues) { 4624 // We need to mark seen separately 4625 if (uiValues.containsKey(UIProvider.ConversationColumns.SEEN)) { 4626 final int seenValue = uiValues.getAsInteger(UIProvider.ConversationColumns.SEEN); 4627 4628 if (seenValue == 1) { 4629 final String mailboxId = uri.getLastPathSegment(); 4630 final int rows = markAllSeen(context, mailboxId); 4631 4632 if (uiValues.size() == 1) { 4633 // Nothing else to do, so return this value 4634 return rows; 4635 } 4636 } 4637 } 4638 4639 final Uri ourUri = convertToEmailProviderUri(uri, Mailbox.CONTENT_URI, true); 4640 if (ourUri == null) return 0; 4641 ContentValues ourValues = new ContentValues(); 4642 // This should only be called via update to "recent folders" 4643 for (String columnName: uiValues.keySet()) { 4644 if (columnName.equals(MailboxColumns.LAST_TOUCHED_TIME)) { 4645 ourValues.put(MailboxColumns.LAST_TOUCHED_TIME, uiValues.getAsLong(columnName)); 4646 } 4647 } 4648 return update(ourUri, ourValues, null, null); 4649 } 4650 4651 private int markAllSeen(final Context context, final String mailboxId) { 4652 final SQLiteDatabase db = getDatabase(context); 4653 final String table = Message.TABLE_NAME; 4654 final ContentValues values = new ContentValues(1); 4655 values.put(MessageColumns.FLAG_SEEN, 1); 4656 final String whereClause = MessageColumns.MAILBOX_KEY + " = ?"; 4657 final String[] whereArgs = new String[] {mailboxId}; 4658 4659 return db.update(table, values, whereClause, whereArgs); 4660 } 4661 4662 private ContentValues convertUiMessageValues(Message message, ContentValues values) { 4663 final ContentValues ourValues = new ContentValues(); 4664 for (String columnName : values.keySet()) { 4665 final Object val = values.get(columnName); 4666 if (columnName.equals(UIProvider.ConversationColumns.STARRED)) { 4667 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_FAVORITE, val); 4668 } else if (columnName.equals(UIProvider.ConversationColumns.READ)) { 4669 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_READ, val); 4670 } else if (columnName.equals(UIProvider.ConversationColumns.SEEN)) { 4671 putIntegerLongOrBoolean(ourValues, MessageColumns.FLAG_SEEN, val); 4672 } else if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4673 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, val); 4674 } else if (columnName.equals(UIProvider.ConversationOperations.FOLDERS_UPDATED)) { 4675 // Skip this column, as the folders will also be specified the RAW_FOLDERS column 4676 } else if (columnName.equals(UIProvider.ConversationColumns.RAW_FOLDERS)) { 4677 // Convert from folder list uri to mailbox key 4678 final FolderList flist = FolderList.fromBlob(values.getAsByteArray(columnName)); 4679 if (flist.folders.size() != 1) { 4680 LogUtils.e(TAG, 4681 "Incorrect number of folders for this message: Message is %s", 4682 message.mId); 4683 } else { 4684 final Folder f = flist.folders.get(0); 4685 final Uri uri = f.folderUri.fullUri; 4686 final Long mailboxId = Long.parseLong(uri.getLastPathSegment()); 4687 putIntegerLongOrBoolean(ourValues, MessageColumns.MAILBOX_KEY, mailboxId); 4688 } 4689 } else if (columnName.equals(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES)) { 4690 Address[] fromList = Address.unpack(message.mFrom); 4691 final MailPrefs mailPrefs = MailPrefs.get(getContext()); 4692 for (Address sender : fromList) { 4693 final String email = sender.getAddress(); 4694 mailPrefs.setDisplayImagesFromSender(email, null); 4695 } 4696 } else if (columnName.equals(UIProvider.ConversationColumns.VIEWED) || 4697 columnName.equals(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO)) { 4698 // Ignore for now 4699 } else if (UIProvider.ConversationColumns.CONVERSATION_INFO.equals(columnName)) { 4700 // Email's conversation info is generated, not stored, so just ignore this update 4701 } else { 4702 throw new IllegalArgumentException("Can't update " + columnName + " in message"); 4703 } 4704 } 4705 return ourValues; 4706 } 4707 4708 private static Uri convertToEmailProviderUri(Uri uri, Uri newBaseUri, boolean asProvider) { 4709 final String idString = uri.getLastPathSegment(); 4710 try { 4711 final long id = Long.parseLong(idString); 4712 Uri ourUri = ContentUris.withAppendedId(newBaseUri, id); 4713 if (asProvider) { 4714 ourUri = ourUri.buildUpon().appendQueryParameter(IS_UIPROVIDER, "true").build(); 4715 } 4716 return ourUri; 4717 } catch (NumberFormatException e) { 4718 return null; 4719 } 4720 } 4721 4722 private Message getMessageFromLastSegment(Uri uri) { 4723 long messageId = Long.parseLong(uri.getLastPathSegment()); 4724 return Message.restoreMessageWithId(getContext(), messageId); 4725 } 4726 4727 /** 4728 * Add an undo operation for the current sequence; if the sequence is newer than what we've had, 4729 * clear out the undo list and start over 4730 * @param uri the uri we're working on 4731 * @param op the ContentProviderOperation to perform upon undo 4732 */ 4733 private void addToSequence(Uri uri, ContentProviderOperation op) { 4734 String sequenceString = uri.getQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER); 4735 if (sequenceString != null) { 4736 int sequence = Integer.parseInt(sequenceString); 4737 if (sequence > mLastSequence) { 4738 // Reset sequence 4739 mLastSequenceOps.clear(); 4740 mLastSequence = sequence; 4741 } 4742 // TODO: Need something to indicate a change isn't ready (undoable) 4743 mLastSequenceOps.add(op); 4744 } 4745 } 4746 4747 // TODO: This should depend on flags on the mailbox... 4748 private static boolean uploadsToServer(Context context, Mailbox m) { 4749 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || 4750 m.mType == Mailbox.TYPE_SEARCH) { 4751 return false; 4752 } 4753 String protocol = Account.getProtocol(context, m.mAccountKey); 4754 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 4755 return (info != null && info.syncChanges); 4756 } 4757 4758 private int uiUpdateMessage(Uri uri, ContentValues values) { 4759 return uiUpdateMessage(uri, values, false); 4760 } 4761 4762 private int uiUpdateMessage(Uri uri, ContentValues values, boolean forceSync) { 4763 Context context = getContext(); 4764 Message msg = getMessageFromLastSegment(uri); 4765 if (msg == null) return 0; 4766 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4767 if (mailbox == null) return 0; 4768 Uri ourBaseUri = 4769 (forceSync || uploadsToServer(context, mailbox)) ? Message.SYNCED_CONTENT_URI : 4770 Message.CONTENT_URI; 4771 Uri ourUri = convertToEmailProviderUri(uri, ourBaseUri, true); 4772 if (ourUri == null) return 0; 4773 4774 // Special case - meeting response 4775 if (values.containsKey(UIProvider.MessageOperations.RESPOND_COLUMN)) { 4776 final EmailServiceProxy service = 4777 EmailServiceUtils.getServiceForAccount(context, mailbox.mAccountKey); 4778 try { 4779 service.sendMeetingResponse(msg.mId, 4780 values.getAsInteger(UIProvider.MessageOperations.RESPOND_COLUMN)); 4781 // Delete the message immediately 4782 uiDeleteMessage(uri); 4783 Utility.showToast(context, R.string.confirm_response); 4784 // Notify box has changed so the deletion is reflected in the UI 4785 notifyUIConversationMailbox(mailbox.mId); 4786 } catch (RemoteException e) { 4787 LogUtils.d(TAG, "Remote exception while sending meeting response"); 4788 } 4789 return 1; 4790 } 4791 4792 // Another special case - deleting a draft. 4793 final String operation = values.getAsString( 4794 UIProvider.ConversationOperations.OPERATION_KEY); 4795 if (UIProvider.ConversationOperations.DISCARD_DRAFTS.equals(operation)) { 4796 uiDeleteMessage(uri); 4797 return 1; 4798 } 4799 4800 ContentValues undoValues = new ContentValues(); 4801 ContentValues ourValues = convertUiMessageValues(msg, values); 4802 for (String columnName: ourValues.keySet()) { 4803 if (columnName.equals(MessageColumns.MAILBOX_KEY)) { 4804 undoValues.put(MessageColumns.MAILBOX_KEY, msg.mMailboxKey); 4805 } else if (columnName.equals(MessageColumns.FLAG_READ)) { 4806 undoValues.put(MessageColumns.FLAG_READ, msg.mFlagRead); 4807 } else if (columnName.equals(MessageColumns.FLAG_SEEN)) { 4808 undoValues.put(MessageColumns.FLAG_SEEN, msg.mFlagSeen); 4809 } else if (columnName.equals(MessageColumns.FLAG_FAVORITE)) { 4810 undoValues.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); 4811 } 4812 } 4813 if (undoValues.size() == 0) { 4814 return -1; 4815 } 4816 final Boolean suppressUndo = 4817 values.getAsBoolean(UIProvider.ConversationOperations.Parameters.SUPPRESS_UNDO); 4818 if (suppressUndo == null || !suppressUndo) { 4819 final ContentProviderOperation op = 4820 ContentProviderOperation.newUpdate(convertToEmailProviderUri( 4821 uri, ourBaseUri, false)) 4822 .withValues(undoValues) 4823 .build(); 4824 addToSequence(uri, op); 4825 } 4826 4827 return update(ourUri, ourValues, null, null); 4828 } 4829 4830 /** 4831 * Projection for use with getting mailbox & account keys for a message. 4832 */ 4833 private static final String[] MESSAGE_KEYS_PROJECTION = 4834 { MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY }; 4835 private static final int MESSAGE_KEYS_MAILBOX_KEY_COLUMN = 0; 4836 private static final int MESSAGE_KEYS_ACCOUNT_KEY_COLUMN = 1; 4837 4838 /** 4839 * Notify necessary UI components in response to a message update. 4840 * @param uri The {@link Uri} for this message update. 4841 * @param messageId The id of the message that's been updated. 4842 * @param values The {@link ContentValues} that were updated in the message. 4843 */ 4844 private void handleMessageUpdateNotifications(final Uri uri, final String messageId, 4845 final ContentValues values) { 4846 if (!uri.getBooleanQueryParameter(IS_UIPROVIDER, false)) { 4847 notifyUIConversation(uri); 4848 } 4849 // TODO: Ideally, also test that the values actually changed. 4850 if (values.containsKey(MessageColumns.FLAG_READ) || 4851 values.containsKey(MessageColumns.MAILBOX_KEY)) { 4852 final Cursor c = query( 4853 Message.CONTENT_URI.buildUpon().appendEncodedPath(messageId).build(), 4854 MESSAGE_KEYS_PROJECTION, null, null, null); 4855 if (c != null) { 4856 try { 4857 if (c.moveToFirst()) { 4858 notifyUIFolder(c.getLong(MESSAGE_KEYS_MAILBOX_KEY_COLUMN), 4859 c.getLong(MESSAGE_KEYS_ACCOUNT_KEY_COLUMN)); 4860 } 4861 } finally { 4862 c.close(); 4863 } 4864 } 4865 } 4866 } 4867 4868 /** 4869 * Perform a "Delete" operation 4870 * @param uri message to delete 4871 * @return number of rows affected 4872 */ 4873 private int uiDeleteMessage(Uri uri) { 4874 final Context context = getContext(); 4875 Message msg = getMessageFromLastSegment(uri); 4876 if (msg == null) return 0; 4877 Mailbox mailbox = Mailbox.restoreMailboxWithId(context, msg.mMailboxKey); 4878 if (mailbox == null) return 0; 4879 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_DRAFTS) { 4880 // We actually delete these, including attachments 4881 AttachmentUtilities.deleteAllAttachmentFiles(context, msg.mAccountKey, msg.mId); 4882 final int r = context.getContentResolver().delete( 4883 ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg.mId), null, null); 4884 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4885 notifyUIMessage(msg.mId); 4886 return r; 4887 } 4888 Mailbox trashMailbox = 4889 Mailbox.restoreMailboxOfType(context, msg.mAccountKey, Mailbox.TYPE_TRASH); 4890 if (trashMailbox == null) { 4891 return 0; 4892 } 4893 ContentValues values = new ContentValues(); 4894 values.put(MessageColumns.MAILBOX_KEY, trashMailbox.mId); 4895 final int r = uiUpdateMessage(uri, values, true); 4896 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 4897 notifyUIMessage(msg.mId); 4898 return r; 4899 } 4900 4901 public static final String PICKER_UI_ACCOUNT = "picker_ui_account"; 4902 public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type"; 4903 // Currently unused 4904 //public static final String PICKER_MESSAGE_ID = "picker_message_id"; 4905 public static final String PICKER_HEADER_ID = "picker_header_id"; 4906 4907 private int pickFolder(Uri uri, int type, int headerId) { 4908 Context context = getContext(); 4909 Long acctId = Long.parseLong(uri.getLastPathSegment()); 4910 // For push imap, for example, we want the user to select the trash mailbox 4911 Cursor ac = query(uiUri("uiaccount", acctId), UIProvider.ACCOUNTS_PROJECTION, 4912 null, null, null); 4913 try { 4914 if (ac.moveToFirst()) { 4915 final com.android.mail.providers.Account uiAccount = 4916 new com.android.mail.providers.Account(ac); 4917 Intent intent = new Intent(context, FolderPickerActivity.class); 4918 intent.putExtra(PICKER_UI_ACCOUNT, uiAccount); 4919 intent.putExtra(PICKER_MAILBOX_TYPE, type); 4920 intent.putExtra(PICKER_HEADER_ID, headerId); 4921 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 4922 context.startActivity(intent); 4923 return 1; 4924 } 4925 return 0; 4926 } finally { 4927 ac.close(); 4928 } 4929 } 4930 4931 private int pickTrashFolder(Uri uri) { 4932 return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title); 4933 } 4934 4935 private int pickSentFolder(Uri uri) { 4936 return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title); 4937 } 4938 4939 private Cursor uiUndo(String[] projection) { 4940 // First see if we have any operations saved 4941 // TODO: Make sure seq matches 4942 if (!mLastSequenceOps.isEmpty()) { 4943 try { 4944 // TODO Always use this projection? Or what's passed in? 4945 // Not sure if UI wants it, but I'm making a cursor of convo uri's 4946 MatrixCursor c = new MatrixCursorWithCachedColumns( 4947 new String[] {UIProvider.ConversationColumns.URI}, 4948 mLastSequenceOps.size()); 4949 for (ContentProviderOperation op: mLastSequenceOps) { 4950 c.addRow(new String[] {op.getUri().toString()}); 4951 } 4952 // Just apply the batch and we're done! 4953 applyBatch(mLastSequenceOps); 4954 // But clear the operations 4955 mLastSequenceOps.clear(); 4956 return c; 4957 } catch (OperationApplicationException e) { 4958 LogUtils.d(TAG, "applyBatch exception"); 4959 } 4960 } 4961 return new MatrixCursorWithCachedColumns(projection, 0); 4962 } 4963 4964 private void notifyUIConversation(Uri uri) { 4965 String id = uri.getLastPathSegment(); 4966 Message msg = Message.restoreMessageWithId(getContext(), Long.parseLong(id)); 4967 if (msg != null) { 4968 notifyUIConversationMailbox(msg.mMailboxKey); 4969 } 4970 } 4971 4972 /** 4973 * Notify about the Mailbox id passed in 4974 * @param id the Mailbox id to be notified 4975 */ 4976 private void notifyUIConversationMailbox(long id) { 4977 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, Long.toString(id)); 4978 Mailbox mailbox = Mailbox.restoreMailboxWithId(getContext(), id); 4979 if (mailbox == null) { 4980 LogUtils.w(TAG, "No mailbox for notification: " + id); 4981 return; 4982 } 4983 // Notify combined inbox... 4984 if (mailbox.mType == Mailbox.TYPE_INBOX) { 4985 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, 4986 EmailProvider.combinedMailboxId(Mailbox.TYPE_INBOX)); 4987 } 4988 notifyWidgets(id); 4989 } 4990 4991 /** 4992 * Notify about the message id passed in 4993 * @param id the message id to be notified 4994 */ 4995 private void notifyUIMessage(long id) { 4996 notifyUI(UIPROVIDER_MESSAGE_NOTIFIER, id); 4997 } 4998 4999 /** 5000 * Notify about the Account id passed in 5001 * @param id the Account id to be notified 5002 */ 5003 private void notifyUIAccount(long id) { 5004 // Notify on the specific account 5005 notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, Long.toString(id)); 5006 5007 // Notify on the all accounts list 5008 notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); 5009 } 5010 5011 // TODO: temporary workaround for ConversationCursor 5012 @Deprecated 5013 private static final int NOTIFY_FOLDER_LOOP_MESSAGE_ID = 0; 5014 @Deprecated 5015 private Handler mFolderNotifierHandler; 5016 5017 /** 5018 * Notify about a folder update. Because folder changes can affect the conversation cursor's 5019 * extras, the conversation must also be notified here. 5020 * @param folderId the folder id to be notified 5021 * @param accountId the account id to be notified (for folder list notification). 5022 */ 5023 private void notifyUIFolder(final String folderId, final long accountId) { 5024 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); 5025 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, folderId); 5026 if (accountId != Account.NO_ACCOUNT) { 5027 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, accountId); 5028 } 5029 5030 // Notify for combined account too 5031 // TODO: might be nice to only notify when an inbox changes 5032 notifyUI(UIPROVIDER_FOLDER_NOTIFIER, 5033 getVirtualMailboxId(COMBINED_ACCOUNT_ID, Mailbox.TYPE_INBOX)); 5034 notifyUI(UIPROVIDER_FOLDERLIST_NOTIFIER, COMBINED_ACCOUNT_ID); 5035 5036 // TODO: temporary workaround for ConversationCursor 5037 synchronized (this) { 5038 if (mFolderNotifierHandler == null) { 5039 mFolderNotifierHandler = new Handler(Looper.getMainLooper(), 5040 new Callback() { 5041 @Override 5042 public boolean handleMessage(final android.os.Message message) { 5043 final String folderId = (String) message.obj; 5044 LogUtils.d(TAG, "Notifying conversation Uri %s twice", folderId); 5045 notifyUI(UIPROVIDER_CONVERSATION_NOTIFIER, folderId); 5046 return true; 5047 } 5048 }); 5049 } 5050 } 5051 mFolderNotifierHandler.removeMessages(NOTIFY_FOLDER_LOOP_MESSAGE_ID); 5052 android.os.Message message = android.os.Message.obtain(mFolderNotifierHandler, 5053 NOTIFY_FOLDER_LOOP_MESSAGE_ID); 5054 message.obj = folderId; 5055 mFolderNotifierHandler.sendMessageDelayed(message, 2000); 5056 } 5057 5058 private void notifyUIFolder(final long folderId, final long accountId) { 5059 notifyUIFolder(Long.toString(folderId), accountId); 5060 } 5061 5062 private void notifyUI(Uri uri, String id) { 5063 final Uri notifyUri = (id != null) ? uri.buildUpon().appendPath(id).build() : uri; 5064 getContext().getContentResolver().notifyChange(notifyUri, null); 5065 } 5066 5067 private void notifyUI(Uri uri, long id) { 5068 notifyUI(uri, Long.toString(id)); 5069 } 5070 5071 private Mailbox getMailbox(final Uri uri) { 5072 final long id = Long.parseLong(uri.getLastPathSegment()); 5073 return Mailbox.restoreMailboxWithId(getContext(), id); 5074 } 5075 5076 /** 5077 * Create an android.accounts.Account object for this account. 5078 * @param accountId id of account to load. 5079 * @return an android.accounts.Account for this account, or null if we can't load it. 5080 */ 5081 private android.accounts.Account getAccountManagerAccount(final long accountId) { 5082 final Context context = getContext(); 5083 final Account account = Account.restoreAccountWithId(context, accountId); 5084 if (account == null) return null; 5085 return getAccountManagerAccount(context, account.mEmailAddress, 5086 account.getProtocol(context)); 5087 } 5088 5089 /** 5090 * Create an android.accounts.Account object for an emailAddress/protocol pair. 5091 * @param context A {@link Context}. 5092 * @param emailAddress The email address we're interested in. 5093 * @param protocol The protocol we're intereted in. 5094 * @return an {@link android.accounts.Account} for this info. 5095 */ 5096 private static android.accounts.Account getAccountManagerAccount(final Context context, 5097 final String emailAddress, final String protocol) { 5098 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 5099 return new android.accounts.Account(emailAddress, info.accountType); 5100 } 5101 5102 /** 5103 * Update an account's periodic sync if the sync interval has changed. 5104 * @param accountId id for the account to update. 5105 * @param values the ContentValues for this update to the account. 5106 */ 5107 private void updateAccountSyncInterval(final long accountId, final ContentValues values) { 5108 final Integer syncInterval = values.getAsInteger(AccountColumns.SYNC_INTERVAL); 5109 if (syncInterval == null) { 5110 // No change to the sync interval. 5111 return; 5112 } 5113 final android.accounts.Account account = getAccountManagerAccount(accountId); 5114 if (account == null) { 5115 // Unable to load the account, or unknown protocol. 5116 return; 5117 } 5118 5119 LogUtils.d(TAG, "Setting sync interval for account " + accountId + " to " + syncInterval + 5120 " minutes"); 5121 5122 // First remove all existing periodic syncs. 5123 final List<PeriodicSync> syncs = 5124 ContentResolver.getPeriodicSyncs(account, EmailContent.AUTHORITY); 5125 for (final PeriodicSync sync : syncs) { 5126 ContentResolver.removePeriodicSync(account, EmailContent.AUTHORITY, sync.extras); 5127 } 5128 5129 // Only positive values of sync interval indicate periodic syncs. The value is in minutes, 5130 // while addPeriodicSync expects its time in seconds. 5131 if (syncInterval > 0) { 5132 ContentResolver.addPeriodicSync(account, EmailContent.AUTHORITY, Bundle.EMPTY, 5133 syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); 5134 } 5135 } 5136 5137 /** 5138 * Request a sync. 5139 * @param account The {@link android.accounts.Account} we want to sync. 5140 * @param mailboxId The mailbox id we want to sync (or one of the special constants in 5141 * {@link Mailbox}). 5142 * @param deltaMessageCount If we're requesting a load more, the number of additional messages 5143 * to sync. 5144 */ 5145 private static void startSync(final android.accounts.Account account, final long mailboxId, 5146 final int deltaMessageCount) { 5147 final Bundle extras = Mailbox.createSyncBundle(mailboxId); 5148 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 5149 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 5150 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 5151 if (deltaMessageCount != 0) { 5152 extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); 5153 } 5154 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, 5155 EmailContent.CONTENT_URI.toString()); 5156 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, 5157 SYNC_STATUS_CALLBACK_METHOD); 5158 ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); 5159 LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(), 5160 extras.toString()); 5161 } 5162 5163 /** 5164 * Request a sync. 5165 * @param mailbox The {@link Mailbox} we want to sync. 5166 * @param deltaMessageCount If we're requesting a load more, the number of additional messages 5167 * to sync. 5168 */ 5169 private void startSync(final Mailbox mailbox, final int deltaMessageCount) { 5170 final android.accounts.Account account = getAccountManagerAccount(mailbox.mAccountKey); 5171 startSync(account, mailbox.mId, deltaMessageCount); 5172 } 5173 5174 /** 5175 * Restart any push operations for an account. 5176 * @param account The {@link android.accounts.Account} we're interested in. 5177 */ 5178 private static void restartPush(final android.accounts.Account account) { 5179 final Bundle extras = new Bundle(); 5180 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 5181 extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); 5182 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 5183 extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true); 5184 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_URI, 5185 EmailContent.CONTENT_URI.toString()); 5186 extras.putString(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD, 5187 SYNC_STATUS_CALLBACK_METHOD); 5188 ContentResolver.requestSync(account, EmailContent.AUTHORITY, extras); 5189 LogUtils.i(TAG, "requestSync EmailProvider startSync %s, %s", account.toString(), 5190 extras.toString()); 5191 } 5192 5193 private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { 5194 if (mailbox != null) { 5195 RefreshStatusMonitor.getInstance(getContext()) 5196 .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() { 5197 @Override 5198 public void onRefreshCompleted(long mailboxId, int result) { 5199 final ContentValues values = new ContentValues(); 5200 values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); 5201 values.put(Mailbox.UI_LAST_SYNC_RESULT, result); 5202 mDatabase.update( 5203 Mailbox.TABLE_NAME, 5204 values, 5205 WHERE_ID, 5206 new String[] { String.valueOf(mailboxId) }); 5207 notifyUIFolder(mailbox.mId, mailbox.mAccountKey); 5208 } 5209 5210 @Override 5211 public void onTimeout(long mailboxId) { 5212 // todo 5213 } 5214 }); 5215 startSync(mailbox, deltaMessageCount); 5216 } 5217 return null; 5218 } 5219 5220 //Number of additional messages to load when a user selects "Load more..." in POP/IMAP boxes 5221 public static final int VISIBLE_LIMIT_INCREMENT = 10; 5222 //Number of additional messages to load when a user selects "Load more..." in a search 5223 public static final int SEARCH_MORE_INCREMENT = 10; 5224 5225 private Cursor uiFolderLoadMore(final Mailbox mailbox) { 5226 if (mailbox == null) return null; 5227 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 5228 // Ask for 10 more messages 5229 mSearchParams.mOffset += SEARCH_MORE_INCREMENT; 5230 runSearchQuery(getContext(), mailbox.mAccountKey, mailbox.mId); 5231 } else { 5232 uiFolderRefresh(mailbox, VISIBLE_LIMIT_INCREMENT); 5233 } 5234 return null; 5235 } 5236 5237 private static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 5238 private SearchParams mSearchParams; 5239 5240 /** 5241 * Returns the search mailbox for the specified account, creating one if necessary 5242 * @return the search mailbox for the passed in account 5243 */ 5244 private Mailbox getSearchMailbox(long accountId) { 5245 Context context = getContext(); 5246 Mailbox m = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SEARCH); 5247 if (m == null) { 5248 m = new Mailbox(); 5249 m.mAccountKey = accountId; 5250 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 5251 m.mFlagVisible = false; 5252 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 5253 m.mSyncInterval = 0; 5254 m.mType = Mailbox.TYPE_SEARCH; 5255 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 5256 m.mParentKey = Mailbox.NO_MAILBOX; 5257 m.save(context); 5258 } 5259 return m; 5260 } 5261 5262 private void runSearchQuery(final Context context, final long accountId, 5263 final long searchMailboxId) { 5264 LogUtils.d(TAG, "runSearchQuery. account: %d mailbox id: %d", 5265 accountId, searchMailboxId); 5266 5267 // Start the search running in the background 5268 new AsyncTask<Void, Void, Void>() { 5269 @Override 5270 public Void doInBackground(Void... params) { 5271 final EmailServiceProxy service = 5272 EmailServiceUtils.getServiceForAccount(context, accountId); 5273 if (service != null) { 5274 try { 5275 final int totalCount = 5276 service.searchMessages(accountId, mSearchParams, searchMailboxId); 5277 5278 // Save away the total count 5279 final ContentValues cv = new ContentValues(1); 5280 cv.put(MailboxColumns.TOTAL_COUNT, totalCount); 5281 update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), cv, 5282 null, null); 5283 LogUtils.d(TAG, "EmailProvider#runSearchQuery. TotalCount to UI: %d", 5284 totalCount); 5285 } catch (RemoteException e) { 5286 LogUtils.e("searchMessages", "RemoteException", e); 5287 } 5288 } 5289 return null; 5290 } 5291 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 5292 } 5293 5294 // TODO: Handle searching for more... 5295 private Cursor uiSearch(Uri uri, String[] projection) { 5296 LogUtils.d(TAG, "runSearchQuery in search %s", uri); 5297 final long accountId = Long.parseLong(uri.getLastPathSegment()); 5298 5299 // TODO: Check the actual mailbox 5300 Mailbox inbox = Mailbox.restoreMailboxOfType(getContext(), accountId, Mailbox.TYPE_INBOX); 5301 if (inbox == null) { 5302 LogUtils.w(Logging.LOG_TAG, "In uiSearch, inbox doesn't exist for account " 5303 + accountId); 5304 5305 return null; 5306 } 5307 5308 String filter = uri.getQueryParameter(UIProvider.SearchQueryParameters.QUERY); 5309 if (filter == null) { 5310 throw new IllegalArgumentException("No query parameter in search query"); 5311 } 5312 5313 // Find/create our search mailbox 5314 Mailbox searchMailbox = getSearchMailbox(accountId); 5315 final long searchMailboxId = searchMailbox.mId; 5316 5317 mSearchParams = new SearchParams(inbox.mId, filter, searchMailboxId); 5318 5319 final Context context = getContext(); 5320 if (mSearchParams.mOffset == 0) { 5321 LogUtils.d(TAG, "deleting existing search results."); 5322 5323 // Delete existing contents of search mailbox 5324 ContentResolver resolver = context.getContentResolver(); 5325 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 5326 null); 5327 final ContentValues cv = new ContentValues(1); 5328 // For now, use the actual query as the name of the mailbox 5329 cv.put(Mailbox.DISPLAY_NAME, mSearchParams.mFilter); 5330 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 5331 cv, null, null); 5332 } 5333 5334 // Start the search running in the background 5335 runSearchQuery(context, accountId, searchMailboxId); 5336 5337 // This will look just like a "normal" folder 5338 return uiQuery(UI_FOLDER, ContentUris.withAppendedId(Mailbox.CONTENT_URI, 5339 searchMailbox.mId), projection, false); 5340 } 5341 5342 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 5343 5344 /** 5345 * Delete an account and clean it up 5346 */ 5347 private int uiDeleteAccount(Uri uri) { 5348 Context context = getContext(); 5349 long accountId = Long.parseLong(uri.getLastPathSegment()); 5350 try { 5351 // Get the account URI. 5352 final Account account = Account.restoreAccountWithId(context, accountId); 5353 if (account == null) { 5354 return 0; // Already deleted? 5355 } 5356 5357 deleteAccountData(context, accountId); 5358 5359 // Now delete the account itself 5360 uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 5361 context.getContentResolver().delete(uri, null, null); 5362 5363 // Clean up 5364 AccountBackupRestore.backup(context); 5365 SecurityPolicy.getInstance(context).reducePolicies(); 5366 MailActivityEmail.setServicesEnabledSync(context); 5367 return 1; 5368 } catch (Exception e) { 5369 LogUtils.w(Logging.LOG_TAG, "Exception while deleting account", e); 5370 } 5371 return 0; 5372 } 5373 5374 private int uiDeleteAccountData(Uri uri) { 5375 Context context = getContext(); 5376 long accountId = Long.parseLong(uri.getLastPathSegment()); 5377 // Get the account URI. 5378 final Account account = Account.restoreAccountWithId(context, accountId); 5379 if (account == null) { 5380 return 0; // Already deleted? 5381 } 5382 deleteAccountData(context, accountId); 5383 return 1; 5384 } 5385 5386 /** Projection used for getting email address for an account. */ 5387 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 5388 5389 private static void deleteAccountData(Context context, long accountId) { 5390 // We will delete PIM data, but by the time the asynchronous call to do that happens, 5391 // the account may have been deleted from the DB. Therefore we have to get the email 5392 // address now and send that, rather than the account id. 5393 final String emailAddress = Utility.getFirstRowString(context, Account.CONTENT_URI, 5394 ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 5395 new String[] {Long.toString(accountId)}, null, 0); 5396 if (emailAddress == null) { 5397 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 5398 } 5399 5400 // Delete synced attachments 5401 AttachmentUtilities.deleteAllAccountAttachmentFiles(context, accountId); 5402 5403 // Delete all mailboxes. 5404 ContentResolver resolver = context.getContentResolver(); 5405 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 5406 resolver.delete(Mailbox.CONTENT_URI, MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 5407 5408 // Delete account sync key. 5409 final ContentValues cv = new ContentValues(); 5410 cv.putNull(Account.SYNC_KEY); 5411 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 5412 5413 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 5414 if (emailAddress != null) { 5415 final IEmailService service = 5416 EmailServiceUtils.getServiceForAccount(context, accountId); 5417 if (service != null) { 5418 try { 5419 service.deleteAccountPIMData(emailAddress); 5420 } catch (final RemoteException e) { 5421 // Can't do anything about this 5422 } 5423 } 5424 } 5425 } 5426 5427 private int[] mSavedWidgetIds = new int[0]; 5428 private final ArrayList<Long> mWidgetNotifyMailboxes = new ArrayList<Long>(); 5429 private AppWidgetManager mAppWidgetManager; 5430 private ComponentName mEmailComponent; 5431 5432 private void notifyWidgets(long mailboxId) { 5433 Context context = getContext(); 5434 // Lazily initialize these 5435 if (mAppWidgetManager == null) { 5436 mAppWidgetManager = AppWidgetManager.getInstance(context); 5437 mEmailComponent = new ComponentName(context, WidgetProvider.getProviderName(context)); 5438 } 5439 5440 // See if we have to populate our array of mailboxes used in widgets 5441 int[] widgetIds = mAppWidgetManager.getAppWidgetIds(mEmailComponent); 5442 if (!Arrays.equals(widgetIds, mSavedWidgetIds)) { 5443 mSavedWidgetIds = widgetIds; 5444 String[][] widgetInfos = BaseWidgetProvider.getWidgetInfo(context, widgetIds); 5445 // widgetInfo now has pairs of account uri/folder uri 5446 mWidgetNotifyMailboxes.clear(); 5447 for (String[] widgetInfo: widgetInfos) { 5448 try { 5449 if (widgetInfo == null || TextUtils.isEmpty(widgetInfo[1])) continue; 5450 long id = Long.parseLong(Uri.parse(widgetInfo[1]).getLastPathSegment()); 5451 if (!isCombinedMailbox(id)) { 5452 // For a regular mailbox, just add it to the list 5453 if (!mWidgetNotifyMailboxes.contains(id)) { 5454 mWidgetNotifyMailboxes.add(id); 5455 } 5456 } else { 5457 switch (getVirtualMailboxType(id)) { 5458 // We only handle the combined inbox in widgets 5459 case Mailbox.TYPE_INBOX: 5460 Cursor c = query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 5461 MailboxColumns.TYPE + "=?", 5462 new String[] {Integer.toString(Mailbox.TYPE_INBOX)}, null); 5463 try { 5464 while (c.moveToNext()) { 5465 mWidgetNotifyMailboxes.add( 5466 c.getLong(Mailbox.ID_PROJECTION_COLUMN)); 5467 } 5468 } finally { 5469 c.close(); 5470 } 5471 break; 5472 } 5473 } 5474 } catch (NumberFormatException e) { 5475 // Move along 5476 } 5477 } 5478 } 5479 5480 // If our mailbox needs to be notified, do so... 5481 if (mWidgetNotifyMailboxes.contains(mailboxId)) { 5482 Intent intent = new Intent(Utils.ACTION_NOTIFY_DATASET_CHANGED); 5483 intent.putExtra(Utils.EXTRA_FOLDER_URI, uiUri("uifolder", mailboxId)); 5484 intent.setType(EMAIL_APP_MIME_TYPE); 5485 context.sendBroadcast(intent); 5486 } 5487 } 5488 5489 @Override 5490 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 5491 Context context = getContext(); 5492 writer.println("Installed services:"); 5493 for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(context)) { 5494 writer.println(" " + info); 5495 } 5496 writer.println(); 5497 writer.println("Accounts: "); 5498 Cursor cursor = query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); 5499 if (cursor.getCount() == 0) { 5500 writer.println(" None"); 5501 } 5502 try { 5503 while (cursor.moveToNext()) { 5504 Account account = new Account(); 5505 account.restore(cursor); 5506 writer.println(" Account " + account.mDisplayName); 5507 HostAuth hostAuth = 5508 HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 5509 if (hostAuth != null) { 5510 writer.println(" Protocol = " + hostAuth.mProtocol + 5511 (TextUtils.isEmpty(account.mProtocolVersion) ? "" : " version " + 5512 account.mProtocolVersion)); 5513 } 5514 } 5515 } finally { 5516 cursor.close(); 5517 } 5518 } 5519 5520 synchronized public Handler getDelayedSyncHandler() { 5521 if (mDelayedSyncHandler == null) { 5522 mDelayedSyncHandler = new Handler(getContext().getMainLooper(), new Callback() { 5523 @Override 5524 public boolean handleMessage(android.os.Message msg) { 5525 synchronized (mDelayedSyncRequests) { 5526 final SyncRequestMessage request = (SyncRequestMessage) msg.obj; 5527 // TODO: It's possible that the account is deleted by the time we get here 5528 // It would be nice if we could validate it before trying to sync 5529 final android.accounts.Account account = request.mAccount; 5530 final Bundle extras = Mailbox.createSyncBundle(request.mMailboxId); 5531 ContentResolver.requestSync(account, request.mAuthority, extras); 5532 LogUtils.i(TAG, "requestSync getDelayedSyncHandler %s, %s", 5533 account.toString(), extras.toString()); 5534 mDelayedSyncRequests.remove(request); 5535 return true; 5536 } 5537 } 5538 }); 5539 } 5540 return mDelayedSyncHandler; 5541 } 5542 5543 private class SyncRequestMessage { 5544 private final String mAuthority; 5545 private final android.accounts.Account mAccount; 5546 private final long mMailboxId; 5547 5548 private SyncRequestMessage(final String authority, final android.accounts.Account account, 5549 final long mailboxId) { 5550 mAuthority = authority; 5551 mAccount = account; 5552 mMailboxId = mailboxId; 5553 } 5554 5555 @Override 5556 public boolean equals(Object o) { 5557 if (this == o) { 5558 return true; 5559 } 5560 if (o == null || getClass() != o.getClass()) { 5561 return false; 5562 } 5563 5564 SyncRequestMessage that = (SyncRequestMessage) o; 5565 5566 return mAccount.equals(that.mAccount) 5567 && mMailboxId == that.mMailboxId 5568 && mAuthority.equals(that.mAuthority); 5569 } 5570 5571 @Override 5572 public int hashCode() { 5573 int result = mAuthority.hashCode(); 5574 result = 31 * result + mAccount.hashCode(); 5575 result = 31 * result + (int) (mMailboxId ^ (mMailboxId >>> 32)); 5576 return result; 5577 } 5578 } 5579} 5580