AccountTypeManager.java revision 851222a96b5d68602fb361ea3527101e893f67e3
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
17package com.android.contacts.model;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.accounts.AuthenticatorDescription;
22import android.accounts.OnAccountsUpdateListener;
23import android.content.BroadcastReceiver;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.IContentService;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.SyncAdapterType;
30import android.content.SyncStatusObserver;
31import android.content.pm.PackageManager;
32import android.content.pm.ResolveInfo;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.Looper;
38import android.os.Message;
39import android.os.RemoteException;
40import android.os.SystemClock;
41import android.provider.ContactsContract;
42import android.text.TextUtils;
43import android.util.Log;
44import android.util.TimingLogger;
45
46import com.android.contacts.ContactsUtils;
47import com.android.contacts.list.ContactListFilterController;
48import com.android.contacts.model.account.AccountType;
49import com.android.contacts.model.account.AccountTypeWithDataSet;
50import com.android.contacts.model.account.AccountWithDataSet;
51import com.android.contacts.model.account.ExchangeAccountType;
52import com.android.contacts.model.account.ExternalAccountType;
53import com.android.contacts.model.account.FallbackAccountType;
54import com.android.contacts.model.account.GoogleAccountType;
55import com.android.contacts.model.dataitem.DataKind;
56import com.android.contacts.util.Constants;
57import com.android.internal.util.Objects;
58import com.google.common.annotations.VisibleForTesting;
59import com.google.common.collect.Lists;
60import com.google.common.collect.Maps;
61import com.google.common.collect.Sets;
62
63import java.util.Collection;
64import java.util.Collections;
65import java.util.Comparator;
66import java.util.HashMap;
67import java.util.List;
68import java.util.Map;
69import java.util.Set;
70import java.util.concurrent.CountDownLatch;
71import 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 */
77public 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     */
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
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()
106    public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
107
108    /**
109     * Returns the list of accounts that are group writable.
110     */
111    public abstract List<AccountWithDataSet> getGroupWritableAccounts();
112
113    public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
114
115    public final AccountType getAccountType(String accountType, String dataSet) {
116        return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
117    }
118
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     */
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     */
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     */
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     */
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
169class 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     */
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
328    public void onStatusChanged(int which) {
329        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
330    }
331
332    public void processBroadcastIntent(Intent intent) {
333        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
334    }
335
336    /* This notification will arrive on the background thread */
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     */
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     */
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.ACCOUNT_TYPE.equals(type)) {
414                    accountType = new ExchangeAccountType(mContext, auth.packageName);
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.
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     */
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
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     */
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
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
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     */
631    private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
632        ensureAccountsLoaded();
633        return mInvitableAccountTypes;
634    }
635
636    @Override
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
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     */
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
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
767        protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
768            return findUsableInvitableAccountTypes(mContext);
769        }
770
771        @Override
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         */
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         */
807        public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
808            return mInvitableAccountTypes;
809        }
810
811        public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
812            mInvitableAccountTypes = map;
813            mTimeLastSet = SystemClock.elapsedRealtime();
814        }
815    }
816}
817