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