AccountTypeManager.java revision f82484911e8dcefb720ca834f79bf17dfba355df
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.contacts.common.model; 18 19import android.accounts.Account; 20import android.accounts.AccountManager; 21import android.accounts.AuthenticatorDescription; 22import android.accounts.OnAccountsUpdateListener; 23import android.content.BroadcastReceiver; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.SyncAdapterType; 29import android.content.SyncStatusObserver; 30import android.content.pm.PackageManager; 31import android.content.pm.ResolveInfo; 32import android.net.Uri; 33import android.os.AsyncTask; 34import android.os.Handler; 35import android.os.HandlerThread; 36import android.os.Looper; 37import android.os.Message; 38import android.os.RemoteException; 39import android.os.SystemClock; 40import android.provider.ContactsContract; 41import android.text.TextUtils; 42import android.util.Log; 43import android.util.TimingLogger; 44 45import com.android.contacts.common.MoreContactUtils; 46import com.android.contacts.common.list.ContactListFilterController; 47import com.android.contacts.common.model.account.AccountType; 48import com.android.contacts.common.model.account.AccountTypeWithDataSet; 49import com.android.contacts.common.model.account.AccountWithDataSet; 50import com.android.contacts.common.model.account.ExchangeAccountType; 51import com.android.contacts.common.model.account.ExternalAccountType; 52import com.android.contacts.common.model.account.FallbackAccountType; 53import com.android.contacts.common.model.account.GoogleAccountType; 54import com.android.contacts.common.model.dataitem.DataKind; 55import com.android.contacts.common.test.NeededForTesting; 56import com.android.contacts.common.util.Constants; 57import com.google.common.annotations.VisibleForTesting; 58import com.google.common.base.Objects; 59import com.google.common.collect.Lists; 60import com.google.common.collect.Maps; 61import com.google.common.collect.Sets; 62 63import java.util.Collection; 64import java.util.Collections; 65import java.util.Comparator; 66import java.util.HashMap; 67import java.util.List; 68import java.util.Map; 69import java.util.Set; 70import java.util.concurrent.CountDownLatch; 71import java.util.concurrent.atomic.AtomicBoolean; 72 73/** 74 * Singleton holder for all parsed {@link AccountType} available on the 75 * system, typically filled through {@link PackageManager} queries. 76 */ 77public abstract class AccountTypeManager { 78 static final String TAG = "AccountTypeManager"; 79 80 private static final Object mInitializationLock = new Object(); 81 private static AccountTypeManager mAccountTypeManager; 82 83 /** 84 * Requests the singleton instance of {@link AccountTypeManager} with data bound from 85 * the available authenticators. This method can safely be called from the UI thread. 86 */ 87 public static AccountTypeManager getInstance(Context context) { 88 synchronized (mInitializationLock) { 89 if (mAccountTypeManager == null) { 90 context = context.getApplicationContext(); 91 mAccountTypeManager = new AccountTypeManagerImpl(context); 92 } 93 } 94 return mAccountTypeManager; 95 } 96 97 /** 98 * Set the instance of account type manager. This is only for and should only be used by unit 99 * tests. While having this method is not ideal, it's simpler than the alternative of 100 * holding this as a service in the ContactsApplication context class. 101 * 102 * @param mockManager The mock AccountTypeManager. 103 */ 104 @NeededForTesting 105 public static void setInstanceForTest(AccountTypeManager mockManager) { 106 synchronized (mInitializationLock) { 107 mAccountTypeManager = mockManager; 108 } 109 } 110 111 /** 112 * Returns the list of all accounts (if contactWritableOnly is false) or just the list of 113 * contact writable accounts (if contactWritableOnly is true). 114 */ 115 // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts() 116 public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly); 117 118 /** 119 * Returns the list of accounts that are group writable. 120 */ 121 public abstract List<AccountWithDataSet> getGroupWritableAccounts(); 122 123 public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet); 124 125 public final AccountType getAccountType(String accountType, String dataSet) { 126 return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet)); 127 } 128 129 public final AccountType getAccountTypeForAccount(AccountWithDataSet account) { 130 return getAccountType(account.getAccountTypeWithDataSet()); 131 } 132 133 /** 134 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s 135 * which support the "invite" feature and have one or more account. 136 * 137 * This is a filtered down and more "usable" list compared to 138 * {@link #getAllInvitableAccountTypes}, where usable is defined as: 139 * (1) making sure that the app that contributed the account type is not disabled 140 * (in order to avoid presenting the user with an option that does nothing), and 141 * (2) that there is at least one raw contact with that account type in the database 142 * (assuming that the user probably doesn't use that account type). 143 * 144 * Warning: Don't use on the UI thread because this can scan the database. 145 */ 146 public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes(); 147 148 /** 149 * Find the best {@link DataKind} matching the requested 150 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 151 * If no direct match found, we try searching {@link FallbackAccountType}. 152 */ 153 public DataKind getKindOrFallback(AccountType type, String mimeType) { 154 return type == null ? null : type.getKindForMimetype(mimeType); 155 } 156 157 /** 158 * Returns all registered {@link AccountType}s, including extension ones. 159 * 160 * @param contactWritableOnly if true, it only returns ones that support writing contacts. 161 */ 162 public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly); 163 164 /** 165 * @param contactWritableOnly if true, it only returns ones that support writing contacts. 166 * @return true when this instance contains the given account. 167 */ 168 public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) { 169 for (AccountWithDataSet account_2 : getAccounts(false)) { 170 if (account.equals(account_2)) { 171 return true; 172 } 173 } 174 return false; 175 } 176} 177 178class AccountTypeManagerImpl extends AccountTypeManager 179 implements OnAccountsUpdateListener, SyncStatusObserver { 180 181 private static final Map<AccountTypeWithDataSet, AccountType> 182 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP = 183 Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>()); 184 185 /** 186 * A sample contact URI used to test whether any activities will respond to an 187 * invitable intent with the given URI as the intent data. This doesn't need to be 188 * specific to a real contact because an app that intercepts the intent should probably do so 189 * for all types of contact URIs. 190 */ 191 private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri( 192 1, "xxx"); 193 194 private Context mContext; 195 private AccountManager mAccountManager; 196 197 private AccountType mFallbackAccountType; 198 199 private List<AccountWithDataSet> mAccounts = Lists.newArrayList(); 200 private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList(); 201 private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList(); 202 private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap(); 203 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes = 204 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; 205 206 private final InvitableAccountTypeCache mInvitableAccountTypeCache; 207 208 /** 209 * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been 210 * initialized. False otherwise. 211 */ 212 private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false); 213 214 /** 215 * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. 216 * False otherwise. 217 */ 218 private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false); 219 220 private static final int MESSAGE_LOAD_DATA = 0; 221 private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1; 222 223 private HandlerThread mListenerThread; 224 private Handler mListenerHandler; 225 226 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 227 private final Runnable mCheckFilterValidityRunnable = new Runnable () { 228 @Override 229 public void run() { 230 ContactListFilterController.getInstance(mContext).checkFilterValidity(true); 231 } 232 }; 233 234 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 235 236 @Override 237 public void onReceive(Context context, Intent intent) { 238 Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent); 239 mListenerHandler.sendMessage(msg); 240 } 241 242 }; 243 244 /* A latch that ensures that asynchronous initialization completes before data is used */ 245 private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1); 246 247 private static final Comparator<Account> ACCOUNT_COMPARATOR = new Comparator<Account>() { 248 @Override 249 public int compare(Account a, Account b) { 250 String aDataSet = null; 251 String bDataSet = null; 252 if (a instanceof AccountWithDataSet) { 253 aDataSet = ((AccountWithDataSet) a).dataSet; 254 } 255 if (b instanceof AccountWithDataSet) { 256 bDataSet = ((AccountWithDataSet) b).dataSet; 257 } 258 259 if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type) 260 && Objects.equal(aDataSet, bDataSet)) { 261 return 0; 262 } else if (b.name == null || b.type == null) { 263 return -1; 264 } else if (a.name == null || a.type == null) { 265 return 1; 266 } else { 267 int diff = a.name.compareTo(b.name); 268 if (diff != 0) { 269 return diff; 270 } 271 diff = a.type.compareTo(b.type); 272 if (diff != 0) { 273 return diff; 274 } 275 276 // Accounts without data sets get sorted before those that have them. 277 if (aDataSet != null) { 278 return bDataSet == null ? 1 : aDataSet.compareTo(bDataSet); 279 } else { 280 return -1; 281 } 282 } 283 } 284 }; 285 286 /** 287 * Internal constructor that only performs initial parsing. 288 */ 289 public AccountTypeManagerImpl(Context context) { 290 mContext = context; 291 mFallbackAccountType = new FallbackAccountType(context); 292 293 mAccountManager = AccountManager.get(mContext); 294 295 mListenerThread = new HandlerThread("AccountChangeListener"); 296 mListenerThread.start(); 297 mListenerHandler = new Handler(mListenerThread.getLooper()) { 298 @Override 299 public void handleMessage(Message msg) { 300 switch (msg.what) { 301 case MESSAGE_LOAD_DATA: 302 loadAccountsInBackground(); 303 break; 304 case MESSAGE_PROCESS_BROADCAST_INTENT: 305 processBroadcastIntent((Intent) msg.obj); 306 break; 307 } 308 } 309 }; 310 311 mInvitableAccountTypeCache = new InvitableAccountTypeCache(); 312 313 // Request updates when packages or accounts change 314 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 315 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 316 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 317 filter.addDataScheme("package"); 318 mContext.registerReceiver(mBroadcastReceiver, filter); 319 IntentFilter sdFilter = new IntentFilter(); 320 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 321 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 322 mContext.registerReceiver(mBroadcastReceiver, sdFilter); 323 324 // Request updates when locale is changed so that the order of each field will 325 // be able to be changed on the locale change. 326 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 327 mContext.registerReceiver(mBroadcastReceiver, filter); 328 329 mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false); 330 331 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); 332 333 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 334 } 335 336 @Override 337 public void onStatusChanged(int which) { 338 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 339 } 340 341 public void processBroadcastIntent(Intent intent) { 342 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 343 } 344 345 /* This notification will arrive on the background thread */ 346 public void onAccountsUpdated(Account[] accounts) { 347 // Refresh to catch any changed accounts 348 loadAccountsInBackground(); 349 } 350 351 /** 352 * Returns instantly if accounts and account types have already been loaded. 353 * Otherwise waits for the background thread to complete the loading. 354 */ 355 void ensureAccountsLoaded() { 356 CountDownLatch latch = mInitializationLatch; 357 if (latch == null) { 358 return; 359 } 360 while (true) { 361 try { 362 latch.await(); 363 return; 364 } catch (InterruptedException e) { 365 Thread.currentThread().interrupt(); 366 } 367 } 368 } 369 370 /** 371 * Loads account list and corresponding account types (potentially with data sets). Always 372 * called on a background thread. 373 */ 374 protected void loadAccountsInBackground() { 375 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 376 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start"); 377 } 378 TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground"); 379 final long startTime = SystemClock.currentThreadTimeMillis(); 380 final long startTimeWall = SystemClock.elapsedRealtime(); 381 382 // Account types, keyed off the account type and data set concatenation. 383 final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = 384 Maps.newHashMap(); 385 386 // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can 387 // be multiple account types (with different data sets) for the same type of account, each 388 // type string may have multiple AccountType entries. 389 final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap(); 390 391 final List<AccountWithDataSet> allAccounts = Lists.newArrayList(); 392 final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList(); 393 final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList(); 394 final Set<String> extensionPackages = Sets.newHashSet(); 395 396 final AccountManager am = mAccountManager; 397 398 final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes(); 399 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); 400 401 // First process sync adapters to find any that provide contact data. 402 for (SyncAdapterType sync : syncs) { 403 if (!ContactsContract.AUTHORITY.equals(sync.authority)) { 404 // Skip sync adapters that don't provide contact data. 405 continue; 406 } 407 408 // Look for the formatting details provided by each sync 409 // adapter, using the authenticator to find general resources. 410 final String type = sync.accountType; 411 final AuthenticatorDescription auth = findAuthenticator(auths, type); 412 if (auth == null) { 413 Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it."); 414 continue; 415 } 416 417 AccountType accountType; 418 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { 419 accountType = new GoogleAccountType(mContext, auth.packageName); 420 } else if (ExchangeAccountType.isExchangeType(type)) { 421 accountType = new ExchangeAccountType(mContext, auth.packageName, type); 422 } else { 423 Log.d(TAG, "Registering external account type=" + type 424 + ", packageName=" + auth.packageName); 425 accountType = new ExternalAccountType(mContext, auth.packageName, false); 426 } 427 if (!accountType.isInitialized()) { 428 if (accountType.isEmbedded()) { 429 throw new IllegalStateException("Problem initializing embedded type " 430 + accountType.getClass().getCanonicalName()); 431 } else { 432 // Skip external account types that couldn't be initialized. 433 continue; 434 } 435 } 436 437 accountType.accountType = auth.type; 438 accountType.titleRes = auth.labelId; 439 accountType.iconRes = auth.iconId; 440 441 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 442 443 // Check to see if the account type knows of any other non-sync-adapter packages 444 // that may provide other data sets of contact data. 445 extensionPackages.addAll(accountType.getExtensionPackageNames()); 446 } 447 448 // If any extension packages were specified, process them as well. 449 if (!extensionPackages.isEmpty()) { 450 Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages"); 451 for (String extensionPackage : extensionPackages) { 452 ExternalAccountType accountType = 453 new ExternalAccountType(mContext, extensionPackage, true); 454 if (!accountType.isInitialized()) { 455 // Skip external account types that couldn't be initialized. 456 continue; 457 } 458 if (!accountType.hasContactsMetadata()) { 459 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 460 + " it doesn't have the CONTACTS_STRUCTURE metadata"); 461 continue; 462 } 463 if (TextUtils.isEmpty(accountType.accountType)) { 464 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 465 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" 466 + " attribute"); 467 continue; 468 } 469 Log.d(TAG, "Registering extension package account type=" 470 + accountType.accountType + ", dataSet=" + accountType.dataSet 471 + ", packageName=" + extensionPackage); 472 473 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 474 } 475 } 476 timings.addSplit("Loaded account types"); 477 478 // Map in accounts to associate the account names with each account type entry. 479 Account[] accounts = mAccountManager.getAccounts(); 480 for (Account account : accounts) { 481 boolean syncable = 482 ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 483 484 if (syncable) { 485 List<AccountType> accountTypes = accountTypesByType.get(account.type); 486 if (accountTypes != null) { 487 // Add an account-with-data-set entry for each account type that is 488 // authenticated by this account. 489 for (AccountType accountType : accountTypes) { 490 AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 491 account.name, account.type, accountType.dataSet); 492 allAccounts.add(accountWithDataSet); 493 if (accountType.areContactsWritable()) { 494 contactWritableAccounts.add(accountWithDataSet); 495 } 496 if (accountType.isGroupMembershipEditable()) { 497 groupWritableAccounts.add(accountWithDataSet); 498 } 499 } 500 } 501 } 502 } 503 504 Collections.sort(allAccounts, ACCOUNT_COMPARATOR); 505 Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR); 506 Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR); 507 508 timings.addSplit("Loaded accounts"); 509 510 synchronized (this) { 511 mAccountTypesWithDataSets = accountTypesByTypeAndDataSet; 512 mAccounts = allAccounts; 513 mContactWritableAccounts = contactWritableAccounts; 514 mGroupWritableAccounts = groupWritableAccounts; 515 mInvitableAccountTypes = findAllInvitableAccountTypes( 516 mContext, allAccounts, accountTypesByTypeAndDataSet); 517 } 518 519 timings.dumpToLog(); 520 final long endTimeWall = SystemClock.elapsedRealtime(); 521 final long endTime = SystemClock.currentThreadTimeMillis(); 522 523 Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, " 524 + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) " 525 + (endTime - startTime) + "ms(cpu)"); 526 527 if (mInitializationLatch != null) { 528 mInitializationLatch.countDown(); 529 mInitializationLatch = null; 530 } 531 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 532 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish"); 533 } 534 535 // Check filter validity since filter may become obsolete after account update. It must be 536 // done from UI thread. 537 mMainThreadHandler.post(mCheckFilterValidityRunnable); 538 } 539 540 // Bookkeeping method for tracking the known account types in the given maps. 541 private void addAccountType(AccountType accountType, 542 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet, 543 Map<String, List<AccountType>> accountTypesByType) { 544 accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); 545 List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType); 546 if (accountsForType == null) { 547 accountsForType = Lists.newArrayList(); 548 } 549 accountsForType.add(accountType); 550 accountTypesByType.put(accountType.accountType, accountsForType); 551 } 552 553 /** 554 * Find a specific {@link AuthenticatorDescription} in the provided list 555 * that matches the given account type. 556 */ 557 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths, 558 String accountType) { 559 for (AuthenticatorDescription auth : auths) { 560 if (accountType.equals(auth.type)) { 561 return auth; 562 } 563 } 564 return null; 565 } 566 567 /** 568 * Return list of all known, contact writable {@link AccountWithDataSet}'s. 569 */ 570 @Override 571 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) { 572 ensureAccountsLoaded(); 573 return contactWritableOnly ? mContactWritableAccounts : mAccounts; 574 } 575 576 /** 577 * Return the list of all known, group writable {@link AccountWithDataSet}'s. 578 */ 579 public List<AccountWithDataSet> getGroupWritableAccounts() { 580 ensureAccountsLoaded(); 581 return mGroupWritableAccounts; 582 } 583 584 /** 585 * Find the best {@link DataKind} matching the requested 586 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 587 * If no direct match found, we try searching {@link FallbackAccountType}. 588 */ 589 @Override 590 public DataKind getKindOrFallback(AccountType type, String mimeType) { 591 ensureAccountsLoaded(); 592 DataKind kind = null; 593 594 // Try finding account type and kind matching request 595 if (type != null) { 596 kind = type.getKindForMimetype(mimeType); 597 } 598 599 if (kind == null) { 600 // Nothing found, so try fallback as last resort 601 kind = mFallbackAccountType.getKindForMimetype(mimeType); 602 } 603 604 if (kind == null) { 605 if (Log.isLoggable(TAG, Log.DEBUG)) { 606 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); 607 } 608 } 609 610 return kind; 611 } 612 613 /** 614 * Return {@link AccountType} for the given account type and data set. 615 */ 616 @Override 617 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { 618 ensureAccountsLoaded(); 619 synchronized (this) { 620 AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet); 621 return type != null ? type : mFallbackAccountType; 622 } 623 } 624 625 /** 626 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s 627 * which support the "invite" feature and have one or more account. This is an unfiltered 628 * list. See {@link #getUsableInvitableAccountTypes()}. 629 */ 630 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() { 631 ensureAccountsLoaded(); 632 return mInvitableAccountTypes; 633 } 634 635 @Override 636 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() { 637 ensureAccountsLoaded(); 638 // Since this method is not thread-safe, it's possible for multiple threads to encounter 639 // the situation where (1) the cache has not been initialized yet or 640 // (2) an async task to refresh the account type list in the cache has already been 641 // started. Hence we use {@link AtomicBoolean}s and return cached values immediately 642 // while we compute the actual result in the background. We use this approach instead of 643 // using "synchronized" because computing the account type list involves a DB read, and 644 // can potentially cause a deadlock situation if this method is called from code which 645 // holds the DB lock. The trade-off of potentially having an incorrect list of invitable 646 // account types for a short period of time seems more manageable than enforcing the 647 // context in which this method is called. 648 649 // Computing the list of usable invitable account types is done on the fly as requested. 650 // If this method has never been called before, then block until the list has been computed. 651 if (!mInvitablesCacheIsInitialized.get()) { 652 mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext)); 653 mInvitablesCacheIsInitialized.set(true); 654 } else { 655 // Otherwise, there is a value in the cache. If the value has expired and 656 // an async task has not already been started by another thread, then kick off a new 657 // async task to compute the list. 658 if (mInvitableAccountTypeCache.isExpired() && 659 mInvitablesTaskIsRunning.compareAndSet(false, true)) { 660 new FindInvitablesTask().execute(); 661 } 662 } 663 664 return mInvitableAccountTypeCache.getCachedValue(); 665 } 666 667 /** 668 * Return all {@link AccountType}s with at least one account which supports "invite", i.e. 669 * its {@link AccountType#getInviteContactActivityClassName()} is not empty. 670 */ 671 @VisibleForTesting 672 static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context, 673 Collection<AccountWithDataSet> accounts, 674 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) { 675 HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(); 676 for (AccountWithDataSet account : accounts) { 677 AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet(); 678 AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet); 679 if (type == null) continue; // just in case 680 if (result.containsKey(accountTypeWithDataSet)) continue; 681 682 if (Log.isLoggable(TAG, Log.DEBUG)) { 683 Log.d(TAG, "Type " + accountTypeWithDataSet 684 + " inviteClass=" + type.getInviteContactActivityClassName()); 685 } 686 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) { 687 result.put(accountTypeWithDataSet, type); 688 } 689 } 690 return Collections.unmodifiableMap(result); 691 } 692 693 /** 694 * Return all usable {@link AccountType}s that support the "invite" feature from the 695 * list of all potential invitable account types (retrieved from 696 * {@link #getAllInvitableAccountTypes}). A usable invitable account type means: 697 * (1) there is at least 1 raw contact in the database with that account type, and 698 * (2) the app contributing the account type is not disabled. 699 * 700 * Warning: Don't use on the UI thread because this can scan the database. 701 */ 702 private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes( 703 Context context) { 704 Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes(); 705 if (allInvitables.isEmpty()) { 706 return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; 707 } 708 709 final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(); 710 result.putAll(allInvitables); 711 712 final PackageManager packageManager = context.getPackageManager(); 713 for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) { 714 AccountType accountType = allInvitables.get(accountTypeWithDataSet); 715 716 // Make sure that account types don't come from apps that are disabled. 717 Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, 718 SAMPLE_CONTACT_URI); 719 if (invitableIntent == null) { 720 result.remove(accountTypeWithDataSet); 721 continue; 722 } 723 ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent, 724 PackageManager.MATCH_DEFAULT_ONLY); 725 if (resolveInfo == null) { 726 // If we can't find an activity to start for this intent, then there's no point in 727 // showing this option to the user. 728 result.remove(accountTypeWithDataSet); 729 continue; 730 } 731 732 // Make sure that there is at least 1 raw contact with this account type. This check 733 // is non-trivial and should not be done on the UI thread. 734 if (!accountTypeWithDataSet.hasData(context)) { 735 result.remove(accountTypeWithDataSet); 736 } 737 } 738 739 return Collections.unmodifiableMap(result); 740 } 741 742 @Override 743 public List<AccountType> getAccountTypes(boolean contactWritableOnly) { 744 ensureAccountsLoaded(); 745 final List<AccountType> accountTypes = Lists.newArrayList(); 746 synchronized (this) { 747 for (AccountType type : mAccountTypesWithDataSets.values()) { 748 if (!contactWritableOnly || type.areContactsWritable()) { 749 accountTypes.add(type); 750 } 751 } 752 } 753 return accountTypes; 754 } 755 756 /** 757 * Background task to find all usable {@link AccountType}s that support the "invite" feature 758 * from the list of all potential invitable account types. Once the work is completed, 759 * the list of account types is stored in the {@link AccountTypeManager}'s 760 * {@link InvitableAccountTypeCache}. 761 */ 762 private class FindInvitablesTask extends AsyncTask<Void, Void, 763 Map<AccountTypeWithDataSet, AccountType>> { 764 765 @Override 766 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) { 767 return findUsableInvitableAccountTypes(mContext); 768 } 769 770 @Override 771 protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) { 772 mInvitableAccountTypeCache.setCachedValue(accountTypes); 773 mInvitablesTaskIsRunning.set(false); 774 } 775 } 776 777 /** 778 * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a 779 * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only 780 * for {@link #TIME_TO_LIVE} milliseconds. 781 */ 782 private static final class InvitableAccountTypeCache { 783 784 /** 785 * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds 786 * has elapsed. 787 */ 788 private static final long TIME_TO_LIVE = 60000; 789 790 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes; 791 792 private long mTimeLastSet; 793 794 /** 795 * Returns true if the data in this cache is stale and needs to be refreshed. Returns false 796 * otherwise. 797 */ 798 public boolean isExpired() { 799 return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE; 800 } 801 802 /** 803 * Returns the cached value. Note that the caller is responsible for checking 804 * {@link #isExpired()} to ensure that the value is not stale. 805 */ 806 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() { 807 return mInvitableAccountTypes; 808 } 809 810 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) { 811 mInvitableAccountTypes = map; 812 mTimeLastSet = SystemClock.elapsedRealtime(); 813 } 814 } 815} 816