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.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      */
     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 
     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()
    100     public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
    101 
    102     /**
    103      * Returns the list of accounts that are group writable.
    104      */
    105     public abstract List<AccountWithDataSet> getGroupWritableAccounts();
    106 
    107     public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
    108 
    109     public final AccountType getAccountType(String accountType, String dataSet) {
    110         return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
    111     }
    112 
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    322     public void onStatusChanged(int which) {
    323         mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
    324     }
    325 
    326     public void processBroadcastIntent(Intent intent) {
    327         mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
    328     }
    329 
    330     /* This notification will arrive on the background thread */
    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      */
    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      */
    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.
    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      */
    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
    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      */
    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
    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
    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      */
    625     private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
    626         ensureAccountsLoaded();
    627         return mInvitableAccountTypes;
    628     }
    629 
    630     @Override
    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
    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      */
    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
    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
    761         protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
    762             return findUsableInvitableAccountTypes(mContext);
    763         }
    764 
    765         @Override
    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          */
    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          */
    801         public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
    802             return mInvitableAccountTypes;
    803         }
    804 
    805         public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
    806             mInvitableAccountTypes = map;
    807             mTimeLastSet = SystemClock.elapsedRealtime();
    808         }
    809     }
    810 }
    811