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