blob: b92d7de63bc95f6d3f76f355fdb138d3c6ceea00 [file] [log] [blame]
Chiao Cheng6c712f42012-11-26 15:35:28 -08001/*
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 Log.w(TAG, "Unknown type=" + type + ", mime=" + mimeType);
617 }
618
619 return kind;
620 }
621
622 /**
623 * Return {@link AccountType} for the given account type and data set.
624 */
625 @Override
626 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
627 ensureAccountsLoaded();
628 synchronized (this) {
629 AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
630 return type != null ? type : mFallbackAccountType;
631 }
632 }
633
634 /**
635 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
636 * which support the "invite" feature and have one or more account. This is an unfiltered
637 * list. See {@link #getUsableInvitableAccountTypes()}.
638 */
639 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
640 ensureAccountsLoaded();
641 return mInvitableAccountTypes;
642 }
643
644 @Override
645 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
646 ensureAccountsLoaded();
647 // Since this method is not thread-safe, it's possible for multiple threads to encounter
648 // the situation where (1) the cache has not been initialized yet or
649 // (2) an async task to refresh the account type list in the cache has already been
650 // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
651 // while we compute the actual result in the background. We use this approach instead of
652 // using "synchronized" because computing the account type list involves a DB read, and
653 // can potentially cause a deadlock situation if this method is called from code which
654 // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
655 // account types for a short period of time seems more manageable than enforcing the
656 // context in which this method is called.
657
658 // Computing the list of usable invitable account types is done on the fly as requested.
659 // If this method has never been called before, then block until the list has been computed.
660 if (!mInvitablesCacheIsInitialized.get()) {
661 mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
662 mInvitablesCacheIsInitialized.set(true);
663 } else {
664 // Otherwise, there is a value in the cache. If the value has expired and
665 // an async task has not already been started by another thread, then kick off a new
666 // async task to compute the list.
667 if (mInvitableAccountTypeCache.isExpired() &&
668 mInvitablesTaskIsRunning.compareAndSet(false, true)) {
669 new FindInvitablesTask().execute();
670 }
671 }
672
673 return mInvitableAccountTypeCache.getCachedValue();
674 }
675
676 /**
677 * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
678 * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
679 */
680 @VisibleForTesting
681 static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context,
682 Collection<AccountWithDataSet> accounts,
683 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
684 HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
685 for (AccountWithDataSet account : accounts) {
686 AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
687 AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
688 if (type == null) continue; // just in case
689 if (result.containsKey(accountTypeWithDataSet)) continue;
690
691 if (Log.isLoggable(TAG, Log.DEBUG)) {
692 Log.d(TAG, "Type " + accountTypeWithDataSet
693 + " inviteClass=" + type.getInviteContactActivityClassName());
694 }
695 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
696 result.put(accountTypeWithDataSet, type);
697 }
698 }
699 return Collections.unmodifiableMap(result);
700 }
701
702 /**
703 * Return all usable {@link AccountType}s that support the "invite" feature from the
704 * list of all potential invitable account types (retrieved from
705 * {@link #getAllInvitableAccountTypes}). A usable invitable account type means:
706 * (1) there is at least 1 raw contact in the database with that account type, and
707 * (2) the app contributing the account type is not disabled.
708 *
709 * Warning: Don't use on the UI thread because this can scan the database.
710 */
711 private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
712 Context context) {
713 Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
714 if (allInvitables.isEmpty()) {
715 return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
716 }
717
718 final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
719 result.putAll(allInvitables);
720
721 final PackageManager packageManager = context.getPackageManager();
722 for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
723 AccountType accountType = allInvitables.get(accountTypeWithDataSet);
724
725 // Make sure that account types don't come from apps that are disabled.
726 Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType,
727 SAMPLE_CONTACT_URI);
728 if (invitableIntent == null) {
729 result.remove(accountTypeWithDataSet);
730 continue;
731 }
732 ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent,
733 PackageManager.MATCH_DEFAULT_ONLY);
734 if (resolveInfo == null) {
735 // If we can't find an activity to start for this intent, then there's no point in
736 // showing this option to the user.
737 result.remove(accountTypeWithDataSet);
738 continue;
739 }
740
741 // Make sure that there is at least 1 raw contact with this account type. This check
742 // is non-trivial and should not be done on the UI thread.
743 if (!accountTypeWithDataSet.hasData(context)) {
744 result.remove(accountTypeWithDataSet);
745 }
746 }
747
748 return Collections.unmodifiableMap(result);
749 }
750
751 @Override
752 public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
753 ensureAccountsLoaded();
754 final List<AccountType> accountTypes = Lists.newArrayList();
755 synchronized (this) {
756 for (AccountType type : mAccountTypesWithDataSets.values()) {
757 if (!contactWritableOnly || type.areContactsWritable()) {
758 accountTypes.add(type);
759 }
760 }
761 }
762 return accountTypes;
763 }
764
765 /**
766 * Background task to find all usable {@link AccountType}s that support the "invite" feature
767 * from the list of all potential invitable account types. Once the work is completed,
768 * the list of account types is stored in the {@link AccountTypeManager}'s
769 * {@link InvitableAccountTypeCache}.
770 */
771 private class FindInvitablesTask extends AsyncTask<Void, Void,
772 Map<AccountTypeWithDataSet, AccountType>> {
773
774 @Override
775 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
776 return findUsableInvitableAccountTypes(mContext);
777 }
778
779 @Override
780 protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
781 mInvitableAccountTypeCache.setCachedValue(accountTypes);
782 mInvitablesTaskIsRunning.set(false);
783 }
784 }
785
786 /**
787 * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a
788 * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only
789 * for {@link #TIME_TO_LIVE} milliseconds.
790 */
791 private static final class InvitableAccountTypeCache {
792
793 /**
794 * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds
795 * has elapsed.
796 */
797 private static final long TIME_TO_LIVE = 60000;
798
799 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
800
801 private long mTimeLastSet;
802
803 /**
804 * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
805 * otherwise.
806 */
807 public boolean isExpired() {
808 return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
809 }
810
811 /**
812 * Returns the cached value. Note that the caller is responsible for checking
813 * {@link #isExpired()} to ensure that the value is not stale.
814 */
815 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
816 return mInvitableAccountTypes;
817 }
818
819 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
820 mInvitableAccountTypes = map;
821 mTimeLastSet = SystemClock.elapsedRealtime();
822 }
823 }
824}