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