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