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