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    */
getInstance(Context context)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    */
setInstanceForTest(AccountTypeManager mockManager)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()
getAccounts(boolean contactWritableOnly)113   public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
114 
115   /** Returns the list of accounts that are group writable. */
getGroupWritableAccounts()116   public abstract List<AccountWithDataSet> getGroupWritableAccounts();
117 
getAccountType(AccountTypeWithDataSet accountTypeWithDataSet)118   public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
119 
getAccountType(String accountType, String dataSet)120   public final AccountType getAccountType(String accountType, String dataSet) {
121     return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
122   }
123 
getAccountTypeForAccount(AccountWithDataSet account)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    */
getUsableInvitableAccountTypes()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    */
getKindOrFallback(AccountType type, String mimeType)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    */
getAccountTypes(boolean contactWritableOnly)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    */
contains(AccountWithDataSet account, boolean contactWritableOnly)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. */
AccountTypeManagerImpl(Context context)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    */
findAuthenticator( AuthenticatorDescription[] auths, String accountType)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
findAllInvitableAccountTypes( Context context, Collection<AccountWithDataSet> accounts, Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet)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
onStatusChanged(int which)363   public void onStatusChanged(int which) {
364     mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
365   }
366 
processBroadcastIntent(Intent intent)367   public void processBroadcastIntent(Intent intent) {
368     mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
369   }
370 
371   /* This notification will arrive on the background thread */
onAccountsUpdated(Account[] accounts)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    */
ensureAccountsLoaded()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    */
loadAccountsInBackground()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.
addAccountType( AccountType accountType, Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet, Map<String, List<AccountType>> accountTypesByType)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
getAccounts(boolean contactWritableOnly)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. */
getGroupWritableAccounts()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
getKindOrFallback(AccountType type, String mimeType)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
getAccountType(AccountTypeWithDataSet accountTypeWithDataSet)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    */
getAllInvitableAccountTypes()657   private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
658     ensureAccountsLoaded();
659     return mInvitableAccountTypes;
660   }
661 
662   @Override
getUsableInvitableAccountTypes()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    */
findUsableInvitableAccountTypes( Context context)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
getAccountTypes(boolean contactWritableOnly)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      */
isExpired()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      */
getCachedValue()784     public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
785       return mInvitableAccountTypes;
786     }
787 
setCachedValue(Map<AccountTypeWithDataSet, AccountType> map)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
doInBackground(Void... params)803     protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
804       return findUsableInvitableAccountTypes(mContext);
805     }
806 
807     @Override
onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes)808     protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
809       mInvitableAccountTypeCache.setCachedValue(accountTypes);
810       mInvitablesTaskIsRunning.set(false);
811     }
812   }
813 }
814