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.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.ContactsUtils; 47 import com.android.contacts.list.ContactListFilterController; 48 import com.android.contacts.model.account.AccountType; 49 import com.android.contacts.model.account.AccountTypeWithDataSet; 50 import com.android.contacts.model.account.AccountWithDataSet; 51 import com.android.contacts.model.account.ExchangeAccountType; 52 import com.android.contacts.model.account.ExternalAccountType; 53 import com.android.contacts.model.account.FallbackAccountType; 54 import com.android.contacts.model.account.GoogleAccountType; 55 import com.android.contacts.model.dataitem.DataKind; 56 import com.android.contacts.util.Constants; 57 import com.android.internal.util.Objects; 58 import com.google.common.annotations.VisibleForTesting; 59 import com.google.common.collect.Lists; 60 import com.google.common.collect.Maps; 61 import com.google.common.collect.Sets; 62 63 import java.util.Collection; 64 import java.util.Collections; 65 import java.util.Comparator; 66 import java.util.HashMap; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Set; 70 import java.util.concurrent.CountDownLatch; 71 import 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 */ 77 public abstract class AccountTypeManager { 78 static final String TAG = "AccountTypeManager"; 79 80 public static final String ACCOUNT_TYPE_SERVICE = "contactAccountTypes"; 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 */ getInstance(Context context)86 public static AccountTypeManager getInstance(Context context) { 87 context = context.getApplicationContext(); 88 AccountTypeManager service = 89 (AccountTypeManager) context.getSystemService(ACCOUNT_TYPE_SERVICE); 90 if (service == null) { 91 service = createAccountTypeManager(context); 92 Log.e(TAG, "No account type service in context: " + context); 93 } 94 return service; 95 } 96 createAccountTypeManager(Context context)97 public static synchronized AccountTypeManager createAccountTypeManager(Context context) { 98 return new AccountTypeManagerImpl(context); 99 } 100 101 /** 102 * Returns the list of all accounts (if contactWritableOnly is false) or just the list of 103 * contact writable accounts (if contactWritableOnly is true). 104 */ 105 // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts() getAccounts(boolean contactWritableOnly)106 public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly); 107 108 /** 109 * Returns the list of accounts that are group writable. 110 */ getGroupWritableAccounts()111 public abstract List<AccountWithDataSet> getGroupWritableAccounts(); 112 getAccountType(AccountTypeWithDataSet accountTypeWithDataSet)113 public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet); 114 getAccountType(String accountType, String dataSet)115 public final AccountType getAccountType(String accountType, String dataSet) { 116 return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet)); 117 } 118 getAccountTypeForAccount(AccountWithDataSet account)119 public final AccountType getAccountTypeForAccount(AccountWithDataSet account) { 120 return getAccountType(account.getAccountTypeWithDataSet()); 121 } 122 123 /** 124 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s 125 * which support the "invite" feature and have one or more account. 126 * 127 * This is a filtered down and more "usable" list compared to 128 * {@link #getAllInvitableAccountTypes}, where usable is defined as: 129 * (1) making sure that the app that contributed the account type is not disabled 130 * (in order to avoid presenting the user with an option that does nothing), and 131 * (2) that there is at least one raw contact with that account type in the database 132 * (assuming that the user probably doesn't use that account type). 133 * 134 * Warning: Don't use on the UI thread because this can scan the database. 135 */ getUsableInvitableAccountTypes()136 public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes(); 137 138 /** 139 * Find the best {@link DataKind} matching the requested 140 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 141 * If no direct match found, we try searching {@link FallbackAccountType}. 142 */ getKindOrFallback(String accountType, String dataSet, String mimeType)143 public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) { 144 final AccountType type = getAccountType(accountType, dataSet); 145 return type == null ? null : type.getKindForMimetype(mimeType); 146 } 147 148 /** 149 * Returns all registered {@link AccountType}s, including extension ones. 150 * 151 * @param contactWritableOnly if true, it only returns ones that support writing contacts. 152 */ getAccountTypes(boolean contactWritableOnly)153 public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly); 154 155 /** 156 * @param contactWritableOnly if true, it only returns ones that support writing contacts. 157 * @return true when this instance contains the given account. 158 */ contains(AccountWithDataSet account, boolean contactWritableOnly)159 public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) { 160 for (AccountWithDataSet account_2 : getAccounts(false)) { 161 if (account.equals(account_2)) { 162 return true; 163 } 164 } 165 return false; 166 } 167 } 168 169 class AccountTypeManagerImpl extends AccountTypeManager 170 implements OnAccountsUpdateListener, SyncStatusObserver { 171 172 private static final Map<AccountTypeWithDataSet, AccountType> 173 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP = 174 Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>()); 175 176 /** 177 * A sample contact URI used to test whether any activities will respond to an 178 * invitable intent with the given URI as the intent data. This doesn't need to be 179 * specific to a real contact because an app that intercepts the intent should probably do so 180 * for all types of contact URIs. 181 */ 182 private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri( 183 1, "xxx"); 184 185 private Context mContext; 186 private AccountManager mAccountManager; 187 188 private AccountType mFallbackAccountType; 189 190 private List<AccountWithDataSet> mAccounts = Lists.newArrayList(); 191 private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList(); 192 private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList(); 193 private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap(); 194 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes = 195 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; 196 197 private final InvitableAccountTypeCache mInvitableAccountTypeCache; 198 199 /** 200 * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been 201 * initialized. False otherwise. 202 */ 203 private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false); 204 205 /** 206 * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. 207 * False otherwise. 208 */ 209 private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false); 210 211 private static final int MESSAGE_LOAD_DATA = 0; 212 private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1; 213 214 private HandlerThread mListenerThread; 215 private Handler mListenerHandler; 216 217 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 218 private final Runnable mCheckFilterValidityRunnable = new Runnable () { 219 @Override 220 public void run() { 221 ContactListFilterController.getInstance(mContext).checkFilterValidity(true); 222 } 223 }; 224 225 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 226 227 @Override 228 public void onReceive(Context context, Intent intent) { 229 Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent); 230 mListenerHandler.sendMessage(msg); 231 } 232 233 }; 234 235 /* A latch that ensures that asynchronous initialization completes before data is used */ 236 private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1); 237 238 private static final Comparator<Account> ACCOUNT_COMPARATOR = new Comparator<Account>() { 239 @Override 240 public int compare(Account a, Account b) { 241 String aDataSet = null; 242 String bDataSet = null; 243 if (a instanceof AccountWithDataSet) { 244 aDataSet = ((AccountWithDataSet) a).dataSet; 245 } 246 if (b instanceof AccountWithDataSet) { 247 bDataSet = ((AccountWithDataSet) b).dataSet; 248 } 249 250 if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type) 251 && Objects.equal(aDataSet, bDataSet)) { 252 return 0; 253 } else if (b.name == null || b.type == null) { 254 return -1; 255 } else if (a.name == null || a.type == null) { 256 return 1; 257 } else { 258 int diff = a.name.compareTo(b.name); 259 if (diff != 0) { 260 return diff; 261 } 262 diff = a.type.compareTo(b.type); 263 if (diff != 0) { 264 return diff; 265 } 266 267 // Accounts without data sets get sorted before those that have them. 268 if (aDataSet != null) { 269 return bDataSet == null ? 1 : aDataSet.compareTo(bDataSet); 270 } else { 271 return -1; 272 } 273 } 274 } 275 }; 276 277 /** 278 * Internal constructor that only performs initial parsing. 279 */ AccountTypeManagerImpl(Context context)280 public AccountTypeManagerImpl(Context context) { 281 mContext = context; 282 mFallbackAccountType = new FallbackAccountType(context); 283 284 mAccountManager = AccountManager.get(mContext); 285 286 mListenerThread = new HandlerThread("AccountChangeListener"); 287 mListenerThread.start(); 288 mListenerHandler = new Handler(mListenerThread.getLooper()) { 289 @Override 290 public void handleMessage(Message msg) { 291 switch (msg.what) { 292 case MESSAGE_LOAD_DATA: 293 loadAccountsInBackground(); 294 break; 295 case MESSAGE_PROCESS_BROADCAST_INTENT: 296 processBroadcastIntent((Intent) msg.obj); 297 break; 298 } 299 } 300 }; 301 302 mInvitableAccountTypeCache = new InvitableAccountTypeCache(); 303 304 // Request updates when packages or accounts change 305 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 306 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 307 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 308 filter.addDataScheme("package"); 309 mContext.registerReceiver(mBroadcastReceiver, filter); 310 IntentFilter sdFilter = new IntentFilter(); 311 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 312 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 313 mContext.registerReceiver(mBroadcastReceiver, sdFilter); 314 315 // Request updates when locale is changed so that the order of each field will 316 // be able to be changed on the locale change. 317 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 318 mContext.registerReceiver(mBroadcastReceiver, filter); 319 320 mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false); 321 322 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); 323 324 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 325 } 326 327 @Override onStatusChanged(int which)328 public void onStatusChanged(int which) { 329 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 330 } 331 processBroadcastIntent(Intent intent)332 public void processBroadcastIntent(Intent intent) { 333 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 334 } 335 336 /* This notification will arrive on the background thread */ onAccountsUpdated(Account[] accounts)337 public void onAccountsUpdated(Account[] accounts) { 338 // Refresh to catch any changed accounts 339 loadAccountsInBackground(); 340 } 341 342 /** 343 * Returns instantly if accounts and account types have already been loaded. 344 * Otherwise waits for the background thread to complete the loading. 345 */ ensureAccountsLoaded()346 void ensureAccountsLoaded() { 347 CountDownLatch latch = mInitializationLatch; 348 if (latch == null) { 349 return; 350 } 351 while (true) { 352 try { 353 latch.await(); 354 return; 355 } catch (InterruptedException e) { 356 Thread.currentThread().interrupt(); 357 } 358 } 359 } 360 361 /** 362 * Loads account list and corresponding account types (potentially with data sets). Always 363 * called on a background thread. 364 */ loadAccountsInBackground()365 protected void loadAccountsInBackground() { 366 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 367 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start"); 368 } 369 TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground"); 370 final long startTime = SystemClock.currentThreadTimeMillis(); 371 final long startTimeWall = SystemClock.elapsedRealtime(); 372 373 // Account types, keyed off the account type and data set concatenation. 374 final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = 375 Maps.newHashMap(); 376 377 // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can 378 // be multiple account types (with different data sets) for the same type of account, each 379 // type string may have multiple AccountType entries. 380 final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap(); 381 382 final List<AccountWithDataSet> allAccounts = Lists.newArrayList(); 383 final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList(); 384 final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList(); 385 final Set<String> extensionPackages = Sets.newHashSet(); 386 387 final AccountManager am = mAccountManager; 388 final IContentService cs = ContentResolver.getContentService(); 389 390 try { 391 final SyncAdapterType[] syncs = cs.getSyncAdapterTypes(); 392 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); 393 394 // First process sync adapters to find any that provide contact data. 395 for (SyncAdapterType sync : syncs) { 396 if (!ContactsContract.AUTHORITY.equals(sync.authority)) { 397 // Skip sync adapters that don't provide contact data. 398 continue; 399 } 400 401 // Look for the formatting details provided by each sync 402 // adapter, using the authenticator to find general resources. 403 final String type = sync.accountType; 404 final AuthenticatorDescription auth = findAuthenticator(auths, type); 405 if (auth == null) { 406 Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it."); 407 continue; 408 } 409 410 AccountType accountType; 411 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { 412 accountType = new GoogleAccountType(mContext, auth.packageName); 413 } else if (ExchangeAccountType.isExchangeType(type)) { 414 accountType = new ExchangeAccountType(mContext, auth.packageName, type); 415 } else { 416 // TODO: use syncadapter package instead, since it provides resources 417 Log.d(TAG, "Registering external account type=" + type 418 + ", packageName=" + auth.packageName); 419 accountType = new ExternalAccountType(mContext, auth.packageName, false); 420 } 421 if (!accountType.isInitialized()) { 422 if (accountType.isEmbedded()) { 423 throw new IllegalStateException("Problem initializing embedded type " 424 + accountType.getClass().getCanonicalName()); 425 } else { 426 // Skip external account types that couldn't be initialized. 427 continue; 428 } 429 } 430 431 accountType.accountType = auth.type; 432 accountType.titleRes = auth.labelId; 433 accountType.iconRes = auth.iconId; 434 435 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 436 437 // Check to see if the account type knows of any other non-sync-adapter packages 438 // that may provide other data sets of contact data. 439 extensionPackages.addAll(accountType.getExtensionPackageNames()); 440 } 441 442 // If any extension packages were specified, process them as well. 443 if (!extensionPackages.isEmpty()) { 444 Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages"); 445 for (String extensionPackage : extensionPackages) { 446 ExternalAccountType accountType = 447 new ExternalAccountType(mContext, extensionPackage, true); 448 if (!accountType.isInitialized()) { 449 // Skip external account types that couldn't be initialized. 450 continue; 451 } 452 if (!accountType.hasContactsMetadata()) { 453 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 454 + " it doesn't have the CONTACTS_STRUCTURE metadata"); 455 continue; 456 } 457 if (TextUtils.isEmpty(accountType.accountType)) { 458 Log.w(TAG, "Skipping extension package " + extensionPackage + " because" 459 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" 460 + " attribute"); 461 continue; 462 } 463 Log.d(TAG, "Registering extension package account type=" 464 + accountType.accountType + ", dataSet=" + accountType.dataSet 465 + ", packageName=" + extensionPackage); 466 467 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 468 } 469 } 470 } catch (RemoteException e) { 471 Log.w(TAG, "Problem loading accounts: " + e.toString()); 472 } 473 timings.addSplit("Loaded account types"); 474 475 // Map in accounts to associate the account names with each account type entry. 476 Account[] accounts = mAccountManager.getAccounts(); 477 for (Account account : accounts) { 478 boolean syncable = false; 479 try { 480 syncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 481 } catch (RemoteException e) { 482 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 483 } 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. addAccountType(AccountType accountType, Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet, Map<String, List<AccountType>> accountTypesByType)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 */ findAuthenticator(AuthenticatorDescription[] auths, String accountType)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 getAccounts(boolean contactWritableOnly)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 */ getGroupWritableAccounts()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 getKindOrFallback(String accountType, String dataSet, String mimeType)591 public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) { 592 ensureAccountsLoaded(); 593 DataKind kind = null; 594 595 // Try finding account type and kind matching request 596 final AccountType type = mAccountTypesWithDataSets.get( 597 AccountTypeWithDataSet.get(accountType, dataSet)); 598 if (type != null) { 599 kind = type.getKindForMimetype(mimeType); 600 } 601 602 if (kind == null) { 603 // Nothing found, so try fallback as last resort 604 kind = mFallbackAccountType.getKindForMimetype(mimeType); 605 } 606 607 if (kind == null) { 608 Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType); 609 } 610 611 return kind; 612 } 613 614 /** 615 * Return {@link AccountType} for the given account type and data set. 616 */ 617 @Override getAccountType(AccountTypeWithDataSet accountTypeWithDataSet)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 */ getAllInvitableAccountTypes()631 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() { 632 ensureAccountsLoaded(); 633 return mInvitableAccountTypes; 634 } 635 636 @Override getUsableInvitableAccountTypes()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 findAllInvitableAccountTypes(Context context, Collection<AccountWithDataSet> accounts, Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet)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 */ findUsableInvitableAccountTypes( Context context)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 = ContactsUtils.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 getAccountTypes(boolean contactWritableOnly)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 doInBackground(Void... params)767 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) { 768 return findUsableInvitableAccountTypes(mContext); 769 } 770 771 @Override onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes)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 */ isExpired()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 */ getCachedValue()807 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() { 808 return mInvitableAccountTypes; 809 } 810 setCachedValue(Map<AccountTypeWithDataSet, AccountType> map)811 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) { 812 mInvitableAccountTypes = map; 813 mTimeLastSet = SystemClock.elapsedRealtime(); 814 } 815 } 816 } 817