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 // TODO: use syncadapter package instead, since it provides resources 424 Log.d(TAG, "Registering external account type=" + type 425 + ", packageName=" + auth.packageName); 426 accountType = new ExternalAccountType(mContext, auth.packageName, false); 427 } 428 if (!accountType.isInitialized()) { 429 if (accountType.isEmbedded()) { 430 throw new IllegalStateException("Problem initializing embedded type " 431 + accountType.getClass().getCanonicalName()); 432 } else { 433 // Skip external account types that couldn't be initialized. 434 continue; 435 } 436 } 437 438 accountType.accountType = auth.type; 439 accountType.titleRes = auth.labelId; 440 accountType.iconRes = auth.iconId; 441 442 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 443 444 // Check to see if the account type knows of any other non-sync-adapter packages 445 // that may provide other data sets of contact data. 446 extensionPackages.addAll(accountType.getExtensionPackageNames()); 447 } 448 449 // If any extension packages were specified, process them as well. 450 if (!extensionPackages.isEmpty()) { 451 Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages"); 452 for (String extensionPackage : extensionPackages) { 453 ExternalAccountType accountType = 454 new ExternalAccountType(mContext, extensionPackage, true); 455 if (!accountType.isInitialized()) { 456 // Skip external account types that couldn't be initialized. 457 continue; 458 } 459 if (!accountType.hasContactsMetadata()) { 460 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 461 + " it doesn't have the CONTACTS_STRUCTURE metadata"); 462 continue; 463 } 464 if (TextUtils.isEmpty(accountType.accountType)) { 465 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 466 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" 467 + " attribute"); 468 continue; 469 } 470 Log.d(TAG, "Registering extension package account type=" 471 + accountType.accountType + ", dataSet=" + accountType.dataSet 472 + ", packageName=" + extensionPackage); 473 474 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 475 } 476 } 477 timings.addSplit("Loaded account types"); 478 479 // Map in accounts to associate the account names with each account type entry. 480 Account[] accounts = mAccountManager.getAccounts(); 481 for (Account account : accounts) { 482 boolean syncable = 483 ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 484 485 if (syncable) { 486 List<AccountType> accountTypes = accountTypesByType.get(account.type); 487 if (accountTypes != null) { 488 // Add an account-with-data-set entry for each account type that is 489 // authenticated by this account. 490 for (AccountType accountType : accountTypes) { 491 AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 492 account.name, account.type, accountType.dataSet); 493 allAccounts.add(accountWithDataSet); 494 if (accountType.areContactsWritable()) { 495 contactWritableAccounts.add(accountWithDataSet); 496 } 497 if (accountType.isGroupMembershipEditable()) { 498 groupWritableAccounts.add(accountWithDataSet); 499 } 500 } 501 } 502 } 503 } 504 505 Collections.sort(allAccounts, ACCOUNT_COMPARATOR); 506 Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR); 507 Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR); 508 509 timings.addSplit("Loaded accounts"); 510 511 synchronized (this) { 512 mAccountTypesWithDataSets = accountTypesByTypeAndDataSet; 513 mAccounts = allAccounts; 514 mContactWritableAccounts = contactWritableAccounts; 515 mGroupWritableAccounts = groupWritableAccounts; 516 mInvitableAccountTypes = findAllInvitableAccountTypes( 517 mContext, allAccounts, accountTypesByTypeAndDataSet); 518 } 519 520 timings.dumpToLog(); 521 final long endTimeWall = SystemClock.elapsedRealtime(); 522 final long endTime = SystemClock.currentThreadTimeMillis(); 523 524 Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, " 525 + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) " 526 + (endTime - startTime) + "ms(cpu)"); 527 528 if (mInitializationLatch != null) { 529 mInitializationLatch.countDown(); 530 mInitializationLatch = null; 531 } 532 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 533 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish"); 534 } 535 536 // Check filter validity since filter may become obsolete after account update. It must be 537 // done from UI thread. 538 mMainThreadHandler.post(mCheckFilterValidityRunnable); 539 } 540 541 // Bookkeeping method for tracking the known account types in the given maps. 542 private void addAccountType(AccountType accountType, 543 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet, 544 Map<String, List<AccountType>> accountTypesByType) { 545 accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); 546 List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType); 547 if (accountsForType == null) { 548 accountsForType = Lists.newArrayList(); 549 } 550 accountsForType.add(accountType); 551 accountTypesByType.put(accountType.accountType, accountsForType); 552 } 553 554 /** 555 * Find a specific {@link AuthenticatorDescription} in the provided list 556 * that matches the given account type. 557 */ 558 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths, 559 String accountType) { 560 for (AuthenticatorDescription auth : auths) { 561 if (accountType.equals(auth.type)) { 562 return auth; 563 } 564 } 565 return null; 566 } 567 568 /** 569 * Return list of all known, contact writable {@link AccountWithDataSet}'s. 570 */ 571 @Override 572 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) { 573 ensureAccountsLoaded(); 574 return contactWritableOnly ? mContactWritableAccounts : mAccounts; 575 } 576 577 /** 578 * Return the list of all known, group writable {@link AccountWithDataSet}'s. 579 */ 580 public List<AccountWithDataSet> getGroupWritableAccounts() { 581 ensureAccountsLoaded(); 582 return mGroupWritableAccounts; 583 } 584 585 /** 586 * Find the best {@link DataKind} matching the requested 587 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 588 * If no direct match found, we try searching {@link FallbackAccountType}. 589 */ 590 @Override 591 public DataKind getKindOrFallback(AccountType type, String mimeType) { 592 ensureAccountsLoaded(); 593 DataKind kind = null; 594 595 // Try finding account type and kind matching request 596 if (type != null) { 597 kind = type.getKindForMimetype(mimeType); 598 } 599 600 if (kind == null) { 601 // Nothing found, so try fallback as last resort 602 kind = mFallbackAccountType.getKindForMimetype(mimeType); 603 } 604 605 if (kind == null) { 606 if (Log.isLoggable(TAG, Log.DEBUG)) { 607 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); 608 } 609 } 610 611 return kind; 612 } 613 614 /** 615 * Return {@link AccountType} for the given account type and data set. 616 */ 617 @Override 618 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { 619 ensureAccountsLoaded(); 620 synchronized (this) { 621 AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet); 622 return type != null ? type : mFallbackAccountType; 623 } 624 } 625 626 /** 627 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s 628 * which support the "invite" feature and have one or more account. This is an unfiltered 629 * list. See {@link #getUsableInvitableAccountTypes()}. 630 */ 631 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() { 632 ensureAccountsLoaded(); 633 return mInvitableAccountTypes; 634 } 635 636 @Override 637 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() { 638 ensureAccountsLoaded(); 639 // Since this method is not thread-safe, it's possible for multiple threads to encounter 640 // the situation where (1) the cache has not been initialized yet or 641 // (2) an async task to refresh the account type list in the cache has already been 642 // started. Hence we use {@link AtomicBoolean}s and return cached values immediately 643 // while we compute the actual result in the background. We use this approach instead of 644 // using "synchronized" because computing the account type list involves a DB read, and 645 // can potentially cause a deadlock situation if this method is called from code which 646 // holds the DB lock. The trade-off of potentially having an incorrect list of invitable 647 // account types for a short period of time seems more manageable than enforcing the 648 // context in which this method is called. 649 650 // Computing the list of usable invitable account types is done on the fly as requested. 651 // If this method has never been called before, then block until the list has been computed. 652 if (!mInvitablesCacheIsInitialized.get()) { 653 mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext)); 654 mInvitablesCacheIsInitialized.set(true); 655 } else { 656 // Otherwise, there is a value in the cache. If the value has expired and 657 // an async task has not already been started by another thread, then kick off a new 658 // async task to compute the list. 659 if (mInvitableAccountTypeCache.isExpired() && 660 mInvitablesTaskIsRunning.compareAndSet(false, true)) { 661 new FindInvitablesTask().execute(); 662 } 663 } 664 665 return mInvitableAccountTypeCache.getCachedValue(); 666 } 667 668 /** 669 * Return all {@link AccountType}s with at least one account which supports "invite", i.e. 670 * its {@link AccountType#getInviteContactActivityClassName()} is not empty. 671 */ 672 @VisibleForTesting 673 static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context, 674 Collection<AccountWithDataSet> accounts, 675 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) { 676 HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(); 677 for (AccountWithDataSet account : accounts) { 678 AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet(); 679 AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet); 680 if (type == null) continue; // just in case 681 if (result.containsKey(accountTypeWithDataSet)) continue; 682 683 if (Log.isLoggable(TAG, Log.DEBUG)) { 684 Log.d(TAG, "Type " + accountTypeWithDataSet 685 + " inviteClass=" + type.getInviteContactActivityClassName()); 686 } 687 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) { 688 result.put(accountTypeWithDataSet, type); 689 } 690 } 691 return Collections.unmodifiableMap(result); 692 } 693 694 /** 695 * Return all usable {@link AccountType}s that support the "invite" feature from the 696 * list of all potential invitable account types (retrieved from 697 * {@link #getAllInvitableAccountTypes}). A usable invitable account type means: 698 * (1) there is at least 1 raw contact in the database with that account type, and 699 * (2) the app contributing the account type is not disabled. 700 * 701 * Warning: Don't use on the UI thread because this can scan the database. 702 */ 703 private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes( 704 Context context) { 705 Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes(); 706 if (allInvitables.isEmpty()) { 707 return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; 708 } 709 710 final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(); 711 result.putAll(allInvitables); 712 713 final PackageManager packageManager = context.getPackageManager(); 714 for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) { 715 AccountType accountType = allInvitables.get(accountTypeWithDataSet); 716 717 // Make sure that account types don't come from apps that are disabled. 718 Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, 719 SAMPLE_CONTACT_URI); 720 if (invitableIntent == null) { 721 result.remove(accountTypeWithDataSet); 722 continue; 723 } 724 ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent, 725 PackageManager.MATCH_DEFAULT_ONLY); 726 if (resolveInfo == null) { 727 // If we can't find an activity to start for this intent, then there's no point in 728 // showing this option to the user. 729 result.remove(accountTypeWithDataSet); 730 continue; 731 } 732 733 // Make sure that there is at least 1 raw contact with this account type. This check 734 // is non-trivial and should not be done on the UI thread. 735 if (!accountTypeWithDataSet.hasData(context)) { 736 result.remove(accountTypeWithDataSet); 737 } 738 } 739 740 return Collections.unmodifiableMap(result); 741 } 742 743 @Override 744 public List<AccountType> getAccountTypes(boolean contactWritableOnly) { 745 ensureAccountsLoaded(); 746 final List<AccountType> accountTypes = Lists.newArrayList(); 747 synchronized (this) { 748 for (AccountType type : mAccountTypesWithDataSets.values()) { 749 if (!contactWritableOnly || type.areContactsWritable()) { 750 accountTypes.add(type); 751 } 752 } 753 } 754 return accountTypes; 755 } 756 757 /** 758 * Background task to find all usable {@link AccountType}s that support the "invite" feature 759 * from the list of all potential invitable account types. Once the work is completed, 760 * the list of account types is stored in the {@link AccountTypeManager}'s 761 * {@link InvitableAccountTypeCache}. 762 */ 763 private class FindInvitablesTask extends AsyncTask<Void, Void, 764 Map<AccountTypeWithDataSet, AccountType>> { 765 766 @Override 767 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) { 768 return findUsableInvitableAccountTypes(mContext); 769 } 770 771 @Override 772 protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) { 773 mInvitableAccountTypeCache.setCachedValue(accountTypes); 774 mInvitablesTaskIsRunning.set(false); 775 } 776 } 777 778 /** 779 * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a 780 * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only 781 * for {@link #TIME_TO_LIVE} milliseconds. 782 */ 783 private static final class InvitableAccountTypeCache { 784 785 /** 786 * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds 787 * has elapsed. 788 */ 789 private static final long TIME_TO_LIVE = 60000; 790 791 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes; 792 793 private long mTimeLastSet; 794 795 /** 796 * Returns true if the data in this cache is stale and needs to be refreshed. Returns false 797 * otherwise. 798 */ 799 public boolean isExpired() { 800 return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE; 801 } 802 803 /** 804 * Returns the cached value. Note that the caller is responsible for checking 805 * {@link #isExpired()} to ensure that the value is not stale. 806 */ 807 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() { 808 return mInvitableAccountTypes; 809 } 810 811 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) { 812 mInvitableAccountTypes = map; 813 mTimeLastSet = SystemClock.elapsedRealtime(); 814 } 815 } 816} 817