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