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