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