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