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