MailAppProvider.java revision bc14a6f8da73a563c27dd99dc433f8c301dfe8ed
1/** 2 * Copyright (c) 2011, Google Inc. 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.mail.providers; 18 19import android.app.Activity; 20import android.content.ContentProvider; 21import android.content.ContentProviderClient; 22import android.content.ContentResolver; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.CursorLoader; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.Loader.OnLoadCompleteListener; 29import android.content.SharedPreferences; 30import android.database.Cursor; 31import android.database.MatrixCursor; 32import android.net.Uri; 33import android.os.Bundle; 34 35import com.android.mail.providers.UIProvider.AccountCursorExtraKeys; 36import com.android.mail.providers.protos.boot.AccountReceiver; 37import com.android.mail.utils.LogTag; 38import com.android.mail.utils.LogUtils; 39import com.android.mail.utils.MatrixCursorWithExtra; 40import com.google.common.collect.ImmutableList; 41import com.google.common.collect.ImmutableSet; 42import com.google.common.collect.Maps; 43import com.google.common.collect.Sets; 44 45import org.json.JSONArray; 46import org.json.JSONException; 47import org.json.JSONObject; 48 49import java.util.LinkedHashMap; 50import java.util.List; 51import java.util.Map; 52import java.util.Set; 53 54 55/** 56 * The Mail App provider allows email providers to register "accounts" and the UI has a single 57 * place to query for the list of accounts. 58 * 59 * During development this will allow new account types to be added, and allow them to be shown in 60 * the application. For example, the mock accounts can be enabled/disabled. 61 * In the future, once other processes can add new accounts, this could allow other "mail" 62 * applications have their content appear within the application 63 */ 64public abstract class MailAppProvider extends ContentProvider 65 implements OnLoadCompleteListener<Cursor>{ 66 67 private static final String SHARED_PREFERENCES_NAME = "MailAppProvider"; 68 private static final String ACCOUNT_LIST_KEY = "accountList"; 69 private static final String LAST_VIEWED_ACCOUNT_KEY = "lastViewedAccount"; 70 private static final String LAST_SENT_FROM_ACCOUNT_KEY = "lastSendFromAccount"; 71 72 /** 73 * Extra used in the result from the activity launched by the intent specified 74 * by {@link #getNoAccountsIntent} to return the list of accounts. The data 75 * specified by this extra key should be a ParcelableArray. 76 */ 77 public static final String ADD_ACCOUNT_RESULT_ACCOUNTS_EXTRA = "addAccountResultAccounts"; 78 79 private final static String LOG_TAG = LogTag.getLogTag(); 80 81 private final LinkedHashMap<Uri, AccountCacheEntry> mAccountCache = 82 new LinkedHashMap<Uri, AccountCacheEntry>(); 83 84 private final Map<Uri, CursorLoader> mCursorLoaderMap = Maps.newHashMap(); 85 86 private ContentResolver mResolver; 87 private static String sAuthority; 88 private static MailAppProvider sInstance; 89 private final static Set<Uri> PENDING_ACCOUNT_URIS = Sets.newHashSet(); 90 91 private volatile boolean mAccountsFullyLoaded = false; 92 93 private SharedPreferences mSharedPrefs; 94 95 /** 96 * Allows the implementing provider to specify the authority for this provider. Email and Gmail 97 * must specify different authorities. 98 */ 99 protected abstract String getAuthority(); 100 101 /** 102 * Authority for the suggestions provider. Email and Gmail must specify different authorities, 103 * much like the implementation of {@link #getAuthority()}. 104 * @return the suggestion authority associated with this provider. 105 */ 106 public abstract String getSuggestionAuthority(); 107 108 /** 109 * Allows the implementing provider to specify an intent that should be used in a call to 110 * {@link Context#startActivityForResult(android.content.Intent)} when the account provider 111 * doesn't return any accounts. 112 * 113 * The result from the {@link Activity} activity should include the list of accounts in 114 * the returned intent, in the 115 116 * @return Intent or null, if the provider doesn't specify a behavior when no accounts are 117 * specified. 118 */ 119 protected abstract Intent getNoAccountsIntent(Context context); 120 121 /** 122 * The cursor returned from a call to {@link android.content.ContentResolver#query()} with this 123 * uri will return a cursor that with columns that are a subset of the columns specified 124 * in {@link UIProvider.ConversationColumns} 125 * The cursor returned by this query can return a {@link android.os.Bundle} 126 * from a call to {@link android.database.Cursor#getExtras()}. This Bundle may have 127 * values with keys listed in {@link AccountCursorExtraKeys} 128 */ 129 public static Uri getAccountsUri() { 130 return Uri.parse("content://" + sAuthority + "/"); 131 } 132 133 public static MailAppProvider getInstance() { 134 return sInstance; 135 } 136 137 /** Default constructor */ 138 protected MailAppProvider() { 139 } 140 141 @Override 142 public boolean onCreate() { 143 sAuthority = getAuthority(); 144 mResolver = getContext().getContentResolver(); 145 146 final Intent intent = new Intent(AccountReceiver.ACTION_PROVIDER_CREATED); 147 getContext().sendBroadcast(intent); 148 149 // Load the previously saved account list 150 loadCachedAccountList(); 151 152 synchronized (PENDING_ACCOUNT_URIS) { 153 sInstance = this; 154 155 // Handle the case where addAccountsForUriAsync was called before 156 // this Provider instance was created 157 final Set<Uri> urisToQery = ImmutableSet.copyOf(PENDING_ACCOUNT_URIS); 158 PENDING_ACCOUNT_URIS.clear(); 159 for (Uri accountQueryUri : urisToQery) { 160 addAccountsForUriAsync(accountQueryUri); 161 } 162 } 163 164 return true; 165 } 166 167 @Override 168 public void shutdown() { 169 synchronized (PENDING_ACCOUNT_URIS) { 170 sInstance = null; 171 } 172 173 for (CursorLoader loader : mCursorLoaderMap.values()) { 174 loader.stopLoading(); 175 } 176 mCursorLoaderMap.clear(); 177 } 178 179 @Override 180 public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, 181 String sortOrder) { 182 // This content provider currently only supports one query (to return the list of accounts). 183 // No reason to check the uri. Currently only checking the projections 184 185 // Validates and returns the projection that should be used. 186 final String[] resultProjection = UIProviderValidator.validateAccountProjection(projection); 187 final Bundle extras = new Bundle(); 188 extras.putInt(AccountCursorExtraKeys.ACCOUNTS_LOADED, mAccountsFullyLoaded ? 1 : 0); 189 190 // Make a copy of the account cache 191 final List<AccountCacheEntry> accountList; 192 synchronized (mAccountCache) { 193 accountList = ImmutableList.copyOf(mAccountCache.values()); 194 } 195 196 final MatrixCursor cursor = 197 new MatrixCursorWithExtra(resultProjection, accountList.size(), extras); 198 199 for (AccountCacheEntry accountEntry : accountList) { 200 final Account account = accountEntry.mAccount; 201 final MatrixCursor.RowBuilder builder = cursor.newRow(); 202 203 for (final String columnName : resultProjection) { 204 final int column = UIProvider.getAccountColumn(columnName); 205 switch (column) { 206 case UIProvider.ACCOUNT_ID_COLUMN: 207 builder.add(Integer.valueOf(0)); 208 break; 209 case UIProvider.ACCOUNT_NAME_COLUMN: 210 builder.add(account.name); 211 break; 212 case UIProvider.ACCOUNT_PROVIDER_VERISON_COLUMN: 213 // TODO fix this 214 builder.add(Integer.valueOf(account.providerVersion)); 215 break; 216 case UIProvider.ACCOUNT_URI_COLUMN: 217 builder.add(account.uri); 218 break; 219 case UIProvider.ACCOUNT_CAPABILITIES_COLUMN: 220 builder.add(Integer.valueOf(account.capabilities)); 221 break; 222 case UIProvider.ACCOUNT_FOLDER_LIST_URI_COLUMN: 223 builder.add(account.folderListUri); 224 break; 225 case UIProvider.ACCOUNT_FULL_FOLDER_LIST_URI_COLUMN: 226 builder.add(account.fullFolderListUri); 227 break; 228 case UIProvider.ACCOUNT_SEARCH_URI_COLUMN: 229 builder.add(account.searchUri); 230 break; 231 case UIProvider.ACCOUNT_FROM_ADDRESSES_COLUMN: 232 builder.add(account.accountFromAddresses); 233 break; 234 case UIProvider.ACCOUNT_SAVE_DRAFT_URI_COLUMN: 235 builder.add(account.saveDraftUri); 236 break; 237 case UIProvider.ACCOUNT_SEND_MESSAGE_URI_COLUMN: 238 builder.add(account.sendMessageUri); 239 break; 240 case UIProvider.ACCOUNT_EXPUNGE_MESSAGE_URI_COLUMN: 241 builder.add(account.expungeMessageUri); 242 break; 243 case UIProvider.ACCOUNT_UNDO_URI_COLUMN: 244 builder.add(account.undoUri); 245 break; 246 case UIProvider.ACCOUNT_SETTINGS_INTENT_URI_COLUMN: 247 builder.add(account.settingsIntentUri); 248 break; 249 case UIProvider.ACCOUNT_HELP_INTENT_URI_COLUMN: 250 builder.add(account.helpIntentUri); 251 break; 252 case UIProvider.ACCOUNT_SEND_FEEDBACK_INTENT_URI_COLUMN: 253 builder.add(account.sendFeedbackIntentUri); 254 break; 255 case UIProvider.ACCOUNT_REAUTHENTICATION_INTENT_URI_COLUMN: 256 builder.add(account.reauthenticationIntentUri); 257 break; 258 case UIProvider.ACCOUNT_SYNC_STATUS_COLUMN: 259 builder.add(Integer.valueOf(account.syncStatus)); 260 break; 261 case UIProvider.ACCOUNT_COMPOSE_INTENT_URI_COLUMN: 262 builder.add(account.composeIntentUri); 263 break; 264 case UIProvider.ACCOUNT_MIME_TYPE_COLUMN: 265 builder.add(account.mimeType); 266 break; 267 case UIProvider.ACCOUNT_RECENT_FOLDER_LIST_URI_COLUMN: 268 builder.add(account.recentFolderListUri); 269 break; 270 case UIProvider.ACCOUNT_DEFAULT_RECENT_FOLDER_LIST_URI_COLUMN: 271 builder.add(account.defaultRecentFolderListUri); 272 break; 273 case UIProvider.ACCOUNT_MANUAL_SYNC_URI_COLUMN: 274 builder.add(account.manualSyncUri); 275 break; 276 case UIProvider.ACCOUNT_VIEW_INTENT_PROXY_URI_COLUMN: 277 builder.add(account.viewIntentProxyUri); 278 break; 279 case UIProvider.ACCOUNT_COOKIE_QUERY_URI_COLUMN: 280 builder.add(account.accoutCookieQueryUri); 281 break; 282 case UIProvider.ACCOUNT_COLOR_COLUMN: 283 builder.add(account.color); 284 break; 285 286 case UIProvider.ACCOUNT_SETTINGS_SIGNATURE_COLUMN: 287 builder.add(account.settings.signature); 288 break; 289 case UIProvider.ACCOUNT_SETTINGS_AUTO_ADVANCE_COLUMN: 290 builder.add(Integer.valueOf(account.settings.getAutoAdvanceSetting())); 291 break; 292 case UIProvider.ACCOUNT_SETTINGS_MESSAGE_TEXT_SIZE_COLUMN: 293 builder.add(Integer.valueOf(account.settings.messageTextSize)); 294 break; 295 case UIProvider.ACCOUNT_SETTINGS_REPLY_BEHAVIOR_COLUMN: 296 builder.add(Integer.valueOf(account.settings.replyBehavior)); 297 break; 298 case UIProvider.ACCOUNT_SETTINGS_HIDE_CHECKBOXES_COLUMN: 299 builder.add(Integer.valueOf(account.settings.hideCheckboxes ? 1 : 0)); 300 break; 301 case UIProvider.ACCOUNT_SETTINGS_CONFIRM_DELETE_COLUMN: 302 builder.add(Integer.valueOf(account.settings.confirmDelete ? 1 : 0)); 303 break; 304 case UIProvider.ACCOUNT_SETTINGS_CONFIRM_ARCHIVE_COLUMN: 305 builder.add(Integer.valueOf(account.settings.confirmArchive ? 1 : 0)); 306 break; 307 case UIProvider.ACCOUNT_SETTINGS_CONFIRM_SEND_COLUMN: 308 builder.add(Integer.valueOf(account.settings.confirmSend ? 1 : 0)); 309 break; 310 case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_COLUMN: 311 builder.add(account.settings.defaultInbox); 312 break; 313 case UIProvider.ACCOUNT_SETTINGS_DEFAULT_INBOX_NAME_COLUMN: 314 builder.add(account.settings.defaultInboxName); 315 break; 316 case UIProvider.ACCOUNT_SETTINGS_SNAP_HEADERS_COLUMN: 317 builder.add(Integer.valueOf(account.settings.snapHeaders)); 318 break; 319 case UIProvider.ACCOUNT_SETTINGS_FORCE_REPLY_FROM_DEFAULT_COLUMN: 320 builder.add(Integer.valueOf(account.settings.forceReplyFromDefault ? 1 : 0)); 321 break; 322 case UIProvider.ACCOUNT_SETTINGS_MAX_ATTACHMENT_SIZE_COLUMN: 323 builder.add(account.settings.maxAttachmentSize); 324 break; 325 case UIProvider.ACCOUNT_SETTINGS_SWIPE_COLUMN: 326 builder.add(account.settings.swipe); 327 break; 328 case UIProvider.ACCOUNT_SETTINGS_PRIORITY_ARROWS_ENABLED_COLUMN: 329 builder.add(Integer.valueOf(account.settings.priorityArrowsEnabled ? 1 : 0)); 330 break; 331 case UIProvider.ACCOUNT_SETTINGS_SETUP_INTENT_URI: 332 builder.add(account.settings.setupIntentUri); 333 break; 334 case UIProvider.ACCOUNT_SETTINGS_CONVERSATION_MODE_COLUMN: 335 builder.add(account.settings.conversationViewMode); 336 break; 337 case UIProvider.ACCOUNT_UPDATE_SETTINGS_URI_COLUMN: 338 builder.add(account.updateSettingsUri); 339 break; 340 default: 341 throw new IllegalStateException("Column not found: " + columnName); 342 } 343 } 344 } 345 346 cursor.setNotificationUri(mResolver, getAccountsUri()); 347 return cursor; 348 } 349 350 @Override 351 public Uri insert(Uri url, ContentValues values) { 352 return url; 353 } 354 355 @Override 356 public int update(Uri url, ContentValues values, String selection, 357 String[] selectionArgs) { 358 return 0; 359 } 360 361 @Override 362 public int delete(Uri url, String selection, String[] selectionArgs) { 363 return 0; 364 } 365 366 @Override 367 public String getType(Uri uri) { 368 return null; 369 } 370 371 /** 372 * Asynchronously adds all of the accounts that are specified by the result set returned by 373 * {@link ContentProvider#query()} for the specified uri. The content provider handling the 374 * query needs to handle the {@link UIProvider.ACCOUNTS_PROJECTION} 375 * Any changes to the underlying provider will automatically be reflected. 376 * @param resolver 377 * @param accountsQueryUri 378 */ 379 public static void addAccountsForUriAsync(Uri accountsQueryUri) { 380 synchronized (PENDING_ACCOUNT_URIS) { 381 final MailAppProvider instance = getInstance(); 382 if (instance != null) { 383 instance.startAccountsLoader(accountsQueryUri); 384 } else { 385 PENDING_ACCOUNT_URIS.add(accountsQueryUri); 386 } 387 } 388 } 389 390 /** 391 * Returns the intent that should be used in a call to 392 * {@link Context#startActivity(android.content.Intent)} when the account provider doesn't 393 * return any accounts 394 * @return Intent or null, if the provider doesn't specify a behavior when no acccounts are 395 * specified. 396 */ 397 public static Intent getNoAccountIntent(Context context) { 398 return getInstance().getNoAccountsIntent(context); 399 } 400 401 private synchronized void startAccountsLoader(Uri accountsQueryUri) { 402 final CursorLoader accountsCursorLoader = new CursorLoader(getContext(), accountsQueryUri, 403 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 404 405 // Listen for the results 406 accountsCursorLoader.registerListener(accountsQueryUri.hashCode(), this); 407 accountsCursorLoader.startLoading(); 408 409 // If there is a previous loader for the given uri, stop it 410 final CursorLoader oldLoader = mCursorLoaderMap.get(accountsQueryUri); 411 if (oldLoader != null) { 412 oldLoader.stopLoading(); 413 } 414 mCursorLoaderMap.put(accountsQueryUri, accountsCursorLoader); 415 } 416 417 public static void addAccount(Account account, Uri accountsQueryUri) { 418 final MailAppProvider provider = getInstance(); 419 if (provider == null) { 420 throw new IllegalStateException("MailAppProvider not intialized"); 421 } 422 provider.addAccountImpl(account, accountsQueryUri, true /* notify */); 423 424 // Cache the updated account list 425 provider.cacheAccountList(); 426 } 427 428 private void addAccountImpl(Account account, Uri accountsQueryUri, boolean notify) { 429 addAccountImpl(account.uri, new AccountCacheEntry(account, accountsQueryUri)); 430 431 // Explicitly calling this out of the synchronized block in case any of the observers get 432 // called synchronously. 433 if (notify) { 434 broadcastAccountChange(); 435 } 436 } 437 438 private void addAccountImpl(Uri key, AccountCacheEntry accountEntry) { 439 synchronized (mAccountCache) { 440 LogUtils.v(LOG_TAG, "adding account %s", accountEntry.mAccount); 441 // LinkedHashMap will not change the iteration order when re-inserting a key 442 mAccountCache.put(key, accountEntry); 443 } 444 } 445 446 private static void broadcastAccountChange() { 447 final MailAppProvider provider = sInstance; 448 449 if (provider != null) { 450 provider.mResolver.notifyChange(getAccountsUri(), null); 451 } 452 } 453 454 /** 455 * Returns the {@link Account#uri} (in String form) of the last viewed account. 456 */ 457 public String getLastViewedAccount() { 458 return getPreferences().getString(LAST_VIEWED_ACCOUNT_KEY, null); 459 } 460 461 /** 462 * Persists the {@link Account#uri} (in String form) of the last viewed account. 463 */ 464 public void setLastViewedAccount(String accountUriStr) { 465 final SharedPreferences.Editor editor = getPreferences().edit(); 466 editor.putString(LAST_VIEWED_ACCOUNT_KEY, accountUriStr); 467 editor.apply(); 468 } 469 470 /** 471 * Returns the {@link Account#uri} (in String form) of the last account the 472 * user compose a message from. 473 */ 474 public String getLastSentFromAccount() { 475 return getPreferences().getString(LAST_SENT_FROM_ACCOUNT_KEY, null); 476 } 477 478 /** 479 * Persists the {@link Account#uri} (in String form) of the last account the 480 * user compose a message from. 481 */ 482 public void setLastSentFromAccount(String accountUriStr) { 483 final SharedPreferences.Editor editor = getPreferences().edit(); 484 editor.putString(LAST_SENT_FROM_ACCOUNT_KEY, accountUriStr); 485 editor.apply(); 486 } 487 488 private void loadCachedAccountList() { 489 JSONArray accounts = null; 490 try { 491 final String accountsJson = getPreferences().getString(ACCOUNT_LIST_KEY, null); 492 if (accountsJson != null) { 493 accounts = new JSONArray(accountsJson); 494 } 495 } catch (Exception e) { 496 LogUtils.e(LOG_TAG, e, "ignoring unparsable accounts cache"); 497 } 498 499 if (accounts == null) { 500 return; 501 } 502 503 for (int i = 0; i < accounts.length(); i++) { 504 try { 505 final AccountCacheEntry accountEntry = new AccountCacheEntry( 506 accounts.getJSONObject(i)); 507 508 if (accountEntry.mAccount.settings == null) { 509 LogUtils.e(LOG_TAG, "Dropping account that doesn't specify settings"); 510 continue; 511 } 512 513 Account account = accountEntry.mAccount; 514 ContentProviderClient client = 515 mResolver.acquireContentProviderClient(account.uri); 516 if (client != null) { 517 client.release(); 518 addAccountImpl(account.uri, accountEntry); 519 } else { 520 LogUtils.e(LOG_TAG, "Dropping account without provider: %s", 521 account.name); 522 } 523 524 } catch (Exception e) { 525 // Unable to create account object, skip to next 526 LogUtils.e(LOG_TAG, e, 527 "Unable to create account object from serialized form"); 528 } 529 } 530 broadcastAccountChange(); 531 } 532 533 private void cacheAccountList() { 534 final List<AccountCacheEntry> accountList; 535 536 synchronized (mAccountCache) { 537 accountList = ImmutableList.copyOf(mAccountCache.values()); 538 } 539 540 final JSONArray arr = new JSONArray(); 541 for (AccountCacheEntry accountEntry : accountList) { 542 arr.put(accountEntry.toJSONObject()); 543 } 544 545 final SharedPreferences.Editor editor = getPreferences().edit(); 546 editor.putString(ACCOUNT_LIST_KEY, arr.toString()); 547 editor.apply(); 548 } 549 550 private SharedPreferences getPreferences() { 551 if (mSharedPrefs == null) { 552 mSharedPrefs = getContext().getSharedPreferences( 553 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 554 } 555 return mSharedPrefs; 556 } 557 558 static public Account getAccountFromAccountUri(Uri accountUri) { 559 MailAppProvider provider = getInstance(); 560 if (provider != null && provider.mAccountsFullyLoaded) { 561 synchronized(provider.mAccountCache) { 562 AccountCacheEntry entry = provider.mAccountCache.get(accountUri); 563 if (entry != null) { 564 return entry.mAccount; 565 } 566 } 567 } 568 return null; 569 } 570 571 @Override 572 public void onLoadComplete(Loader<Cursor> loader, Cursor data) { 573 if (data == null) { 574 LogUtils.d(LOG_TAG, "null account cursor returned"); 575 return; 576 } 577 578 LogUtils.d(LOG_TAG, "Cursor with %d accounts returned", data.getCount()); 579 final CursorLoader cursorLoader = (CursorLoader)loader; 580 final Uri accountsQueryUri = cursorLoader.getUri(); 581 582 // preserve ordering on partial updates 583 // also preserve ordering on complete updates for any that existed previously 584 585 586 final List<AccountCacheEntry> accountList; 587 synchronized (mAccountCache) { 588 accountList = ImmutableList.copyOf(mAccountCache.values()); 589 } 590 591 // Build a set of the account uris that had been associated with that query 592 final Set<Uri> previousQueryUriSet = Sets.newHashSet(); 593 for (AccountCacheEntry entry : accountList) { 594 if (accountsQueryUri.equals(entry.mAccountsQueryUri)) { 595 previousQueryUriSet.add(entry.mAccount.uri); 596 } 597 } 598 599 // Update the internal state of this provider if the returned result set 600 // represents all accounts 601 // TODO: determine what should happen with a heterogeneous set of accounts 602 final Bundle extra = data.getExtras(); 603 mAccountsFullyLoaded = extra.getInt(AccountCursorExtraKeys.ACCOUNTS_LOADED) != 0; 604 605 final Set<Uri> newQueryUriMap = Sets.newHashSet(); 606 607 // We are relying on the fact that all accounts are added in the order specified in the 608 // cursor. Initially assume that we insert these items to at the end of the list 609 while (data.moveToNext()) { 610 final Account account = new Account(data); 611 final Uri accountUri = account.uri; 612 newQueryUriMap.add(accountUri); 613 // preserve existing order if already present and this is a partial update, 614 // otherwise add to the end 615 // 616 // N.B. this ordering policy means the order in which providers respond will affect 617 // the order of accounts. 618 if (mAccountsFullyLoaded) { 619 synchronized (mAccountCache) { 620 // removing the existing item will prevent LinkedHashMap from preserving the 621 // original insertion order 622 mAccountCache.remove(accountUri); 623 } 624 } 625 addAccountImpl(account, accountsQueryUri, false /* don't notify */); 626 } 627 // Remove all of the accounts that are in the new result set 628 previousQueryUriSet.removeAll(newQueryUriMap); 629 630 // For all of the entries that had been in the previous result set, and are not 631 // in the new result set, remove them from the cache 632 if (previousQueryUriSet.size() > 0 && mAccountsFullyLoaded) { 633 synchronized (mAccountCache) { 634 for (Uri accountUri : previousQueryUriSet) { 635 LogUtils.d(LOG_TAG, "Removing account %s", accountUri); 636 mAccountCache.remove(accountUri); 637 } 638 } 639 } 640 broadcastAccountChange(); 641 642 // Cache the updated account list 643 cacheAccountList(); 644 } 645 646 /** 647 * Object that allows the Account Cache provider to associate the account with the content 648 * provider uri that originated that account. 649 */ 650 private static class AccountCacheEntry { 651 final Account mAccount; 652 final Uri mAccountsQueryUri; 653 654 private static final String KEY_ACCOUNT = "acct"; 655 private static final String KEY_QUERY_URI = "queryUri"; 656 657 public AccountCacheEntry(Account account, Uri accountQueryUri) { 658 mAccount = account; 659 mAccountsQueryUri = accountQueryUri; 660 } 661 662 public AccountCacheEntry(JSONObject o) throws JSONException { 663 mAccount = Account.newinstance(o.getString(KEY_ACCOUNT)); 664 if (mAccount == null) { 665 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. " 666 + "Account object could not be created from the JSONObject: " 667 + o); 668 } 669 if (mAccount.settings == Settings.EMPTY_SETTINGS) { 670 throw new IllegalArgumentException("AccountCacheEntry de-serializing failed. " 671 + "Settings could not be created from the JSONObject: " + o); 672 } 673 final String uriStr = o.optString(KEY_QUERY_URI, null); 674 if (uriStr != null) { 675 mAccountsQueryUri = Uri.parse(uriStr); 676 } else { 677 mAccountsQueryUri = null; 678 } 679 } 680 681 public JSONObject toJSONObject() { 682 try { 683 return new JSONObject() 684 .put(KEY_ACCOUNT, mAccount.serialize()) 685 .putOpt(KEY_QUERY_URI, mAccountsQueryUri); 686 } catch (JSONException e) { 687 // shouldn't happen 688 throw new IllegalArgumentException(e); 689 } 690 } 691 692 } 693} 694