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