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