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