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