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