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