blob: f4cb428e961cb8c6d787d729987b536b7b92ca12 [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;
Ihab Awad413589f2014-07-02 14:00:28 -070020import android.accounts.AccountManager;
Chiao Cheng6c712f42012-11-26 15:35:28 -080021import android.accounts.AuthenticatorDescription;
22import android.accounts.OnAccountsUpdateListener;
23import android.content.BroadcastReceiver;
24import android.content.ContentResolver;
25import android.content.Context;
Chiao Cheng6c712f42012-11-26 15:35:28 -080026import android.content.Intent;
27import android.content.IntentFilter;
Wenyi Wang56c8a0c2016-09-30 11:11:10 -070028import android.content.SharedPreferences;
Chiao Cheng6c712f42012-11-26 15:35:28 -080029import android.content.SyncAdapterType;
30import android.content.SyncStatusObserver;
31import android.content.pm.PackageManager;
32import android.content.pm.ResolveInfo;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070033import android.database.ContentObserver;
Chiao Cheng6c712f42012-11-26 15:35:28 -080034import android.net.Uri;
35import android.os.AsyncTask;
36import android.os.Handler;
37import android.os.HandlerThread;
38import android.os.Looper;
39import android.os.Message;
Chiao Cheng6c712f42012-11-26 15:35:28 -080040import android.os.SystemClock;
41import android.provider.ContactsContract;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070042import android.support.v4.content.ContextCompat;
Chiao Cheng6c712f42012-11-26 15:35:28 -080043import android.text.TextUtils;
44import android.util.Log;
45import android.util.TimingLogger;
46
Wenyi Wang56c8a0c2016-09-30 11:11:10 -070047import com.android.contacts.R;
Marcus Hagerott511504f2016-11-15 13:58:34 -080048import com.android.contacts.common.Experiments;
Chiao Cheng6c712f42012-11-26 15:35:28 -080049import com.android.contacts.common.MoreContactUtils;
50import com.android.contacts.common.list.ContactListFilterController;
51import com.android.contacts.common.model.account.AccountType;
52import com.android.contacts.common.model.account.AccountTypeWithDataSet;
53import com.android.contacts.common.model.account.AccountWithDataSet;
54import com.android.contacts.common.model.account.ExchangeAccountType;
55import com.android.contacts.common.model.account.ExternalAccountType;
56import com.android.contacts.common.model.account.FallbackAccountType;
57import com.android.contacts.common.model.account.GoogleAccountType;
Brian Attwell684318f2015-02-25 12:39:28 -080058import com.android.contacts.common.model.account.SamsungAccountType;
Chiao Cheng6c712f42012-11-26 15:35:28 -080059import com.android.contacts.common.model.dataitem.DataKind;
Chiao Cheng6c712f42012-11-26 15:35:28 -080060import com.android.contacts.common.util.Constants;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070061import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
Marcus Hagerott6caf23f2016-08-18 15:02:42 -070062import com.android.contactsbind.ObjectFactory;
Marcus Hagerott511504f2016-11-15 13:58:34 -080063import com.android.contactsbind.experiments.Flags;
Chiao Cheng6c712f42012-11-26 15:35:28 -080064import com.google.common.annotations.VisibleForTesting;
65import com.google.common.base.Objects;
Marcus Hagerott67a06392016-10-13 15:16:58 -070066import com.google.common.base.Predicate;
67import com.google.common.collect.Collections2;
Chiao Cheng6c712f42012-11-26 15:35:28 -080068import com.google.common.collect.Lists;
69import com.google.common.collect.Maps;
70import com.google.common.collect.Sets;
71
Gary Maiac333592016-09-28 17:27:40 -070072import java.util.ArrayList;
Chiao Cheng6c712f42012-11-26 15:35:28 -080073import java.util.Collection;
74import java.util.Collections;
75import java.util.Comparator;
76import java.util.HashMap;
77import java.util.List;
78import java.util.Map;
79import java.util.Set;
80import java.util.concurrent.CountDownLatch;
81import java.util.concurrent.atomic.AtomicBoolean;
82
Marcus Hagerott67a06392016-10-13 15:16:58 -070083import javax.annotation.Nullable;
84
Marcus Hagerottfac695a2016-08-24 17:02:40 -070085import static com.android.contacts.common.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
86
Chiao Cheng6c712f42012-11-26 15:35:28 -080087/**
88 * Singleton holder for all parsed {@link AccountType} available on the
89 * system, typically filled through {@link PackageManager} queries.
90 */
91public abstract class AccountTypeManager {
92 static final String TAG = "AccountTypeManager";
93
94 private static final Object mInitializationLock = new Object();
95 private static AccountTypeManager mAccountTypeManager;
96
97 /**
98 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
99 * the available authenticators. This method can safely be called from the UI thread.
100 */
101 public static AccountTypeManager getInstance(Context context) {
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700102 if (!hasRequiredPermissions(context)) {
103 // Hopefully any component that depends on the values returned by this class
104 // will be restarted if the permissions change.
105 return EMPTY;
106 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800107 synchronized (mInitializationLock) {
108 if (mAccountTypeManager == null) {
109 context = context.getApplicationContext();
Marcus Hagerott6caf23f2016-08-18 15:02:42 -0700110 mAccountTypeManager = new AccountTypeManagerImpl(context,
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700111 ObjectFactory.getDeviceLocalAccountTypeFactory(context));
Chiao Cheng6c712f42012-11-26 15:35:28 -0800112 }
113 }
114 return mAccountTypeManager;
115 }
116
117 /**
118 * Set the instance of account type manager. This is only for and should only be used by unit
119 * tests. While having this method is not ideal, it's simpler than the alternative of
120 * holding this as a service in the ContactsApplication context class.
121 *
122 * @param mockManager The mock AccountTypeManager.
123 */
Chiao Cheng6c712f42012-11-26 15:35:28 -0800124 public static void setInstanceForTest(AccountTypeManager mockManager) {
125 synchronized (mInitializationLock) {
126 mAccountTypeManager = mockManager;
127 }
128 }
129
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700130 private static final AccountTypeManager EMPTY = new AccountTypeManager() {
131 @Override
132 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
133 return Collections.emptyList();
134 }
135
136 @Override
Marcus Hagerott67a06392016-10-13 15:16:58 -0700137 public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
138 return Collections.emptyList();
139 }
140
141 @Override
Gary Maiac333592016-09-28 17:27:40 -0700142 public List<AccountWithDataSet> getGroupWritableAccounts() {
143 return Collections.emptyList();
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700144 }
145
146 @Override
Wenyi Wang56c8a0c2016-09-30 11:11:10 -0700147 public Account getDefaultGoogleAccount() {
148 return null;
149 }
150
151 @Override
Gary Maiac333592016-09-28 17:27:40 -0700152 public List<AccountWithDataSet> getSortedAccounts(AccountWithDataSet defaultAccount,
153 boolean contactWritableOnly) {
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700154 return Collections.emptyList();
155 }
156
157 @Override
158 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
159 return null;
160 }
161
162 @Override
163 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
164 return null;
165 }
166
167 @Override
168 public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
169 return Collections.emptyList();
170 }
171 };
172
Chiao Cheng6c712f42012-11-26 15:35:28 -0800173 /**
174 * Returns the list of all accounts (if contactWritableOnly is false) or just the list of
175 * contact writable accounts (if contactWritableOnly is true).
176 */
177 // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
178 public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
179
Marcus Hagerott67a06392016-10-13 15:16:58 -0700180 public abstract List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter);
181
Gary Maiac333592016-09-28 17:27:40 -0700182 public abstract List<AccountWithDataSet> getSortedAccounts(AccountWithDataSet defaultAccount,
183 boolean contactWritableOnly);
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700184
185 /**
Chiao Cheng6c712f42012-11-26 15:35:28 -0800186 * Returns the list of accounts that are group writable.
187 */
188 public abstract List<AccountWithDataSet> getGroupWritableAccounts();
189
Wenyi Wang56c8a0c2016-09-30 11:11:10 -0700190 /**
191 * Returns the default google account.
192 */
193 public abstract Account getDefaultGoogleAccount();
194
195 static Account getDefaultGoogleAccount(AccountManager accountManager,
196 SharedPreferences prefs, String defaultAccountKey) {
197 // Get all the google accounts on the device
198 final Account[] accounts = accountManager.getAccountsByType(
199 GoogleAccountType.ACCOUNT_TYPE);
200 if (accounts == null || accounts.length == 0) {
201 return null;
202 }
203
204 // Get the default account from preferences
205 final String defaultAccount = prefs.getString(defaultAccountKey, null);
206 final AccountWithDataSet accountWithDataSet = defaultAccount == null ? null :
207 AccountWithDataSet.unstringify(defaultAccount);
208
209 // Look for an account matching the one from preferences
210 if (accountWithDataSet != null) {
211 for (int i = 0; i < accounts.length; i++) {
212 if (TextUtils.equals(accountWithDataSet.name, accounts[i].name)
213 && TextUtils.equals(accountWithDataSet.type, accounts[i].type)) {
214 return accounts[i];
215 }
216 }
217 }
218
219 // Just return the first one
220 return accounts[0];
221 }
222
Chiao Cheng6c712f42012-11-26 15:35:28 -0800223 public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
224
225 public final AccountType getAccountType(String accountType, String dataSet) {
226 return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
227 }
228
229 public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
Jay Shrauner2ae200d2015-01-09 11:36:20 -0800230 if (account != null) {
231 return getAccountType(account.getAccountTypeWithDataSet());
232 }
233 return getAccountType(null, null);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800234 }
235
236 /**
237 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
238 * which support the "invite" feature and have one or more account.
239 *
240 * This is a filtered down and more "usable" list compared to
241 * {@link #getAllInvitableAccountTypes}, where usable is defined as:
242 * (1) making sure that the app that contributed the account type is not disabled
243 * (in order to avoid presenting the user with an option that does nothing), and
244 * (2) that there is at least one raw contact with that account type in the database
245 * (assuming that the user probably doesn't use that account type).
246 *
247 * Warning: Don't use on the UI thread because this can scan the database.
248 */
249 public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
250
251 /**
252 * Find the best {@link DataKind} matching the requested
253 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
254 * If no direct match found, we try searching {@link FallbackAccountType}.
255 */
256 public DataKind getKindOrFallback(AccountType type, String mimeType) {
257 return type == null ? null : type.getKindForMimetype(mimeType);
258 }
259
260 /**
261 * Returns all registered {@link AccountType}s, including extension ones.
262 *
263 * @param contactWritableOnly if true, it only returns ones that support writing contacts.
264 */
265 public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
266
267 /**
268 * @param contactWritableOnly if true, it only returns ones that support writing contacts.
269 * @return true when this instance contains the given account.
270 */
271 public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
Tingting Wang0ac73ba2016-07-05 22:33:01 -0700272 for (AccountWithDataSet account_2 : getAccounts(contactWritableOnly)) {
Chiao Cheng6c712f42012-11-26 15:35:28 -0800273 if (account.equals(account_2)) {
274 return true;
275 }
276 }
277 return false;
278 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700279
Marcus Hagerott596eb7e2016-10-18 10:25:12 -0700280 public boolean hasGoogleAccount() {
281 return getDefaultGoogleAccount() != null;
282 }
283
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700284 private static boolean hasRequiredPermissions(Context context) {
285 final boolean canGetAccounts = ContextCompat.checkSelfPermission(context,
286 android.Manifest.permission.GET_ACCOUNTS) == PackageManager.PERMISSION_GRANTED;
287 final boolean canReadContacts = ContextCompat.checkSelfPermission(context,
288 android.Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
289 return canGetAccounts && canReadContacts;
290 }
291
Marcus Hagerott67a06392016-10-13 15:16:58 -0700292 public static Predicate<AccountWithDataSet> nonNullAccountFilter() {
293 return new Predicate<AccountWithDataSet>() {
294 @Override
295 public boolean apply(@Nullable AccountWithDataSet account) {
296 return account != null && account.name != null && account.type != null;
297 }
298 };
299 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800300}
301
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700302class AccountComparator implements Comparator<AccountWithDataSet> {
303 private AccountWithDataSet mDefaultAccount;
304
305 public AccountComparator(AccountWithDataSet defaultAccount) {
306 mDefaultAccount = defaultAccount;
307 }
308
309 @Override
310 public int compare(AccountWithDataSet a, AccountWithDataSet b) {
311 if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
312 && Objects.equal(a.dataSet, b.dataSet)) {
313 return 0;
314 } else if (b.name == null || b.type == null) {
315 return -1;
316 } else if (a.name == null || a.type == null) {
317 return 1;
318 } else if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
319 return -1;
320 } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
321 return 1;
322 } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
323 return -1;
324 } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
325 return 1;
326 } else {
327 int diff = a.name.compareToIgnoreCase(b.name);
328 if (diff != 0) {
329 return diff;
330 }
331 diff = a.type.compareToIgnoreCase(b.type);
332 if (diff != 0) {
333 return diff;
334 }
335
336 // Accounts without data sets get sorted before those that have them.
337 if (a.dataSet != null) {
338 return b.dataSet == null ? 1 : a.dataSet.compareToIgnoreCase(b.dataSet);
339 } else {
340 return -1;
341 }
342 }
343 }
344
345 private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
346 return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
347 }
348}
349
Chiao Cheng6c712f42012-11-26 15:35:28 -0800350class AccountTypeManagerImpl extends AccountTypeManager
351 implements OnAccountsUpdateListener, SyncStatusObserver {
352
353 private static final Map<AccountTypeWithDataSet, AccountType>
354 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
355 Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
356
357 /**
358 * A sample contact URI used to test whether any activities will respond to an
359 * invitable intent with the given URI as the intent data. This doesn't need to be
360 * specific to a real contact because an app that intercepts the intent should probably do so
361 * for all types of contact URIs.
362 */
363 private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(
364 1, "xxx");
365
366 private Context mContext;
Ihab Awad413589f2014-07-02 14:00:28 -0700367 private AccountManager mAccountManager;
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700368 private DeviceLocalAccountTypeFactory mDeviceLocalAccountTypeFactory;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800369
370 private AccountType mFallbackAccountType;
371
372 private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
373 private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
374 private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
375 private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
376 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
377 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
378
379 private final InvitableAccountTypeCache mInvitableAccountTypeCache;
380
381 /**
382 * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
383 * initialized. False otherwise.
384 */
385 private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
386
387 /**
388 * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing.
389 * False otherwise.
390 */
391 private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
392
393 private static final int MESSAGE_LOAD_DATA = 0;
394 private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
395
396 private HandlerThread mListenerThread;
397 private Handler mListenerHandler;
398
399 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
400 private final Runnable mCheckFilterValidityRunnable = new Runnable () {
401 @Override
402 public void run() {
403 ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
404 }
405 };
406
407 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
408
409 @Override
410 public void onReceive(Context context, Intent intent) {
411 Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
412 mListenerHandler.sendMessage(msg);
413 }
414
415 };
416
417 /* A latch that ensures that asynchronous initialization completes before data is used */
418 private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
419
Chiao Cheng6c712f42012-11-26 15:35:28 -0800420 /**
421 * Internal constructor that only performs initial parsing.
422 */
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700423 public AccountTypeManagerImpl(Context context,
424 DeviceLocalAccountTypeFactory deviceLocalAccountTypeFactory) {
Chiao Cheng6c712f42012-11-26 15:35:28 -0800425 mContext = context;
426 mFallbackAccountType = new FallbackAccountType(context);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700427 mDeviceLocalAccountTypeFactory = deviceLocalAccountTypeFactory;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800428
Ihab Awad413589f2014-07-02 14:00:28 -0700429 mAccountManager = AccountManager.get(mContext);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800430
431 mListenerThread = new HandlerThread("AccountChangeListener");
432 mListenerThread.start();
433 mListenerHandler = new Handler(mListenerThread.getLooper()) {
434 @Override
435 public void handleMessage(Message msg) {
436 switch (msg.what) {
437 case MESSAGE_LOAD_DATA:
438 loadAccountsInBackground();
439 break;
440 case MESSAGE_PROCESS_BROADCAST_INTENT:
441 processBroadcastIntent((Intent) msg.obj);
442 break;
443 }
444 }
445 };
446
447 mInvitableAccountTypeCache = new InvitableAccountTypeCache();
448
449 // Request updates when packages or accounts change
450 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
451 filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
452 filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
453 filter.addDataScheme("package");
454 mContext.registerReceiver(mBroadcastReceiver, filter);
455 IntentFilter sdFilter = new IntentFilter();
456 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
457 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
458 mContext.registerReceiver(mBroadcastReceiver, sdFilter);
459
460 // Request updates when locale is changed so that the order of each field will
461 // be able to be changed on the locale change.
462 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
463 mContext.registerReceiver(mBroadcastReceiver, filter);
464
Ihab Awad413589f2014-07-02 14:00:28 -0700465 mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800466
467 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
468
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700469
Marcus Hagerott511504f2016-11-15 13:58:34 -0800470 if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
471 // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
472 // if a new device contact is added.
473 mContext.getContentResolver().registerContentObserver(
474 ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
475 new ContentObserver(mListenerHandler) {
476 @Override
477 public boolean deliverSelfNotifications() {
478 return true;
479 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700480
Marcus Hagerott511504f2016-11-15 13:58:34 -0800481 @Override
482 public void onChange(boolean selfChange) {
483 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
484 }
485
486 @Override
487 public void onChange(boolean selfChange, Uri uri) {
488 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
489 }
490 });
491 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700492
Chiao Cheng6c712f42012-11-26 15:35:28 -0800493 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
494 }
495
496 @Override
497 public void onStatusChanged(int which) {
498 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
499 }
500
501 public void processBroadcastIntent(Intent intent) {
502 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
503 }
504
505 /* This notification will arrive on the background thread */
506 public void onAccountsUpdated(Account[] accounts) {
507 // Refresh to catch any changed accounts
508 loadAccountsInBackground();
509 }
510
511 /**
512 * Returns instantly if accounts and account types have already been loaded.
513 * Otherwise waits for the background thread to complete the loading.
514 */
515 void ensureAccountsLoaded() {
516 CountDownLatch latch = mInitializationLatch;
517 if (latch == null) {
518 return;
519 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700520
Chiao Cheng6c712f42012-11-26 15:35:28 -0800521 while (true) {
522 try {
523 latch.await();
524 return;
525 } catch (InterruptedException e) {
526 Thread.currentThread().interrupt();
527 }
528 }
529 }
530
531 /**
532 * Loads account list and corresponding account types (potentially with data sets). Always
533 * called on a background thread.
534 */
535 protected void loadAccountsInBackground() {
536 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
537 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
538 }
539 TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
540 final long startTime = SystemClock.currentThreadTimeMillis();
541 final long startTimeWall = SystemClock.elapsedRealtime();
542
543 // Account types, keyed off the account type and data set concatenation.
544 final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet =
545 Maps.newHashMap();
546
547 // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can
548 // be multiple account types (with different data sets) for the same type of account, each
549 // type string may have multiple AccountType entries.
550 final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
551
552 final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
553 final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
554 final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
555 final Set<String> extensionPackages = Sets.newHashSet();
556
Ihab Awad413589f2014-07-02 14:00:28 -0700557 final AccountManager am = mAccountManager;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800558
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700559 final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
560 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
Chiao Cheng6c712f42012-11-26 15:35:28 -0800561
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700562 // First process sync adapters to find any that provide contact data.
563 for (SyncAdapterType sync : syncs) {
564 if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
565 // Skip sync adapters that don't provide contact data.
566 continue;
567 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800568
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700569 // Look for the formatting details provided by each sync
570 // adapter, using the authenticator to find general resources.
571 final String type = sync.accountType;
572 final AuthenticatorDescription auth = findAuthenticator(auths, type);
573 if (auth == null) {
574 Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
575 continue;
576 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800577
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700578 AccountType accountType;
579 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
580 accountType = new GoogleAccountType(mContext, auth.packageName);
581 } else if (ExchangeAccountType.isExchangeType(type)) {
582 accountType = new ExchangeAccountType(mContext, auth.packageName, type);
Brian Attwell684318f2015-02-25 12:39:28 -0800583 } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
584 auth.packageName)) {
585 accountType = new SamsungAccountType(mContext, auth.packageName, type);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700586 } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
587 && isLocalAccountType(mDeviceLocalAccountTypeFactory, type)) {
588 // This will be loaded by the DeviceLocalAccountLocator so don't try to create an
589 // ExternalAccountType for it.
590 continue;
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700591 } else {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700592 Log.d(TAG, "Registering external account type=" + type
593 + ", packageName=" + auth.packageName);
594 accountType = new ExternalAccountType(mContext, auth.packageName, false);
595 }
596 if (!accountType.isInitialized()) {
597 if (accountType.isEmbedded()) {
598 throw new IllegalStateException("Problem initializing embedded type "
599 + accountType.getClass().getCanonicalName());
Chiao Cheng6c712f42012-11-26 15:35:28 -0800600 } else {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700601 // Skip external account types that couldn't be initialized.
602 continue;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800603 }
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700604 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800605
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700606 accountType.initializeFieldsFromAuthenticator(auth);
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700607
608 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
609
610 // Check to see if the account type knows of any other non-sync-adapter packages
611 // that may provide other data sets of contact data.
612 extensionPackages.addAll(accountType.getExtensionPackageNames());
613 }
614
615 // If any extension packages were specified, process them as well.
616 if (!extensionPackages.isEmpty()) {
617 Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
618 for (String extensionPackage : extensionPackages) {
619 ExternalAccountType accountType =
620 new ExternalAccountType(mContext, extensionPackage, true);
621 if (!accountType.isInitialized()) {
622 // Skip external account types that couldn't be initialized.
623 continue;
624 }
625 if (!accountType.hasContactsMetadata()) {
626 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
627 + " it doesn't have the CONTACTS_STRUCTURE metadata");
628 continue;
629 }
630 if (TextUtils.isEmpty(accountType.accountType)) {
631 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
632 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
633 + " attribute");
634 continue;
635 }
636 Log.d(TAG, "Registering extension package account type="
637 + accountType.accountType + ", dataSet=" + accountType.dataSet
638 + ", packageName=" + extensionPackage);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800639
640 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800641 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800642 }
643 timings.addSplit("Loaded account types");
644
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700645 boolean foundWritableGoogleAccount = false;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800646 // Map in accounts to associate the account names with each account type entry.
Ihab Awad413589f2014-07-02 14:00:28 -0700647 Account[] accounts = mAccountManager.getAccounts();
Chiao Cheng6c712f42012-11-26 15:35:28 -0800648 for (Account account : accounts) {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700649 boolean syncable =
650 ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800651
Walter Jangf7d733a2016-08-12 14:23:25 -0700652 if (syncable || GoogleAccountType.ACCOUNT_TYPE.equals(account.type)) {
Chiao Cheng6c712f42012-11-26 15:35:28 -0800653 List<AccountType> accountTypes = accountTypesByType.get(account.type);
654 if (accountTypes != null) {
655 // Add an account-with-data-set entry for each account type that is
656 // authenticated by this account.
657 for (AccountType accountType : accountTypes) {
658 AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
659 account.name, account.type, accountType.dataSet);
660 allAccounts.add(accountWithDataSet);
661 if (accountType.areContactsWritable()) {
662 contactWritableAccounts.add(accountWithDataSet);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700663 if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
664 && accountWithDataSet.dataSet == null) {
665 foundWritableGoogleAccount = true;
666 }
Walter Jang0fed9b62016-09-07 15:23:22 -0700667
668 if (accountType.isGroupMembershipEditable()) {
669 groupWritableAccounts.add(accountWithDataSet);
670 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800671 }
672 }
673 }
674 }
675 }
676
Marcus Hagerott7a756ab2016-11-01 18:16:02 -0700677 final DeviceLocalAccountLocator deviceAccountLocator = DeviceLocalAccountLocator
678 .create(mContext, allAccounts);
679 final List<AccountWithDataSet> localAccounts = deviceAccountLocator
680 .getDeviceLocalAccounts();
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700681 allAccounts.addAll(localAccounts);
682
683 for (AccountWithDataSet localAccount : localAccounts) {
684 // Prefer a known type if it exists. This covers the case that a local account has an
685 // authenticator with a valid contacts.xml
686 AccountType localAccountType = accountTypesByTypeAndDataSet.get(
687 localAccount.getAccountTypeWithDataSet());
688 if (localAccountType == null) {
689 localAccountType = mDeviceLocalAccountTypeFactory.getAccountType(localAccount.type);
690 }
691 accountTypesByTypeAndDataSet.put(localAccount.getAccountTypeWithDataSet(),
692 localAccountType);
693
694 // Skip the null account if there is a Google account available. This is done because
695 // the Google account's sync adapter will automatically move accounts in the "null"
696 // account. Hence, it would be confusing to still show it as an available writable
697 // account since contacts that were saved to it would magically change accounts when the
698 // sync adapter runs.
699 if (foundWritableGoogleAccount && localAccount.type == null) {
700 continue;
701 }
702 if (localAccountType.areContactsWritable()) {
703 contactWritableAccounts.add(localAccount);
Walter Jang0fed9b62016-09-07 15:23:22 -0700704
705 if (localAccountType.isGroupMembershipEditable()) {
706 groupWritableAccounts.add(localAccount);
707 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700708 }
709 }
710
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700711 final AccountComparator accountComparator = new AccountComparator(null);
712 Collections.sort(allAccounts, accountComparator);
713 Collections.sort(contactWritableAccounts, accountComparator);
714 Collections.sort(groupWritableAccounts, accountComparator);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800715
716 timings.addSplit("Loaded accounts");
717
718 synchronized (this) {
719 mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
720 mAccounts = allAccounts;
721 mContactWritableAccounts = contactWritableAccounts;
722 mGroupWritableAccounts = groupWritableAccounts;
723 mInvitableAccountTypes = findAllInvitableAccountTypes(
724 mContext, allAccounts, accountTypesByTypeAndDataSet);
725 }
726
727 timings.dumpToLog();
728 final long endTimeWall = SystemClock.elapsedRealtime();
729 final long endTime = SystemClock.currentThreadTimeMillis();
730
731 Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
732 + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) "
733 + (endTime - startTime) + "ms(cpu)");
734
735 if (mInitializationLatch != null) {
736 mInitializationLatch.countDown();
737 mInitializationLatch = null;
738 }
739 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
740 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
741 }
742
743 // Check filter validity since filter may become obsolete after account update. It must be
744 // done from UI thread.
745 mMainThreadHandler.post(mCheckFilterValidityRunnable);
746 }
747
748 // Bookkeeping method for tracking the known account types in the given maps.
749 private void addAccountType(AccountType accountType,
750 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
751 Map<String, List<AccountType>> accountTypesByType) {
752 accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
753 List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
754 if (accountsForType == null) {
755 accountsForType = Lists.newArrayList();
756 }
757 accountsForType.add(accountType);
758 accountTypesByType.put(accountType.accountType, accountsForType);
759 }
760
761 /**
762 * Find a specific {@link AuthenticatorDescription} in the provided list
763 * that matches the given account type.
764 */
765 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
766 String accountType) {
767 for (AuthenticatorDescription auth : auths) {
768 if (accountType.equals(auth.type)) {
769 return auth;
770 }
771 }
772 return null;
773 }
774
775 /**
Gary Maiac333592016-09-28 17:27:40 -0700776 * Return list of all known or contact writable {@link AccountWithDataSet}'s.
777 * {@param contactWritableOnly} whether to restrict to contact writable accounts only
Chiao Cheng6c712f42012-11-26 15:35:28 -0800778 */
779 @Override
780 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
781 ensureAccountsLoaded();
Gary Maiac333592016-09-28 17:27:40 -0700782 return Lists.newArrayList(contactWritableOnly ? mContactWritableAccounts : mAccounts);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800783 }
784
Marcus Hagerott67a06392016-10-13 15:16:58 -0700785 @Override
786 public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
787 return new ArrayList<>(Collections2.filter(mAccounts, filter));
788 }
789
Chiao Cheng6c712f42012-11-26 15:35:28 -0800790 /**
Gary Maiac333592016-09-28 17:27:40 -0700791 * Return list of all known or contact writable {@link AccountWithDataSet}'s sorted by
792 * {@code defaultAccount}.
793 * {@param defaultAccount} account to sort by
794 * {@param contactWritableOnly} whether to restrict to contact writable accounts only
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700795 */
796 @Override
Gary Maiac333592016-09-28 17:27:40 -0700797 public List<AccountWithDataSet> getSortedAccounts(AccountWithDataSet defaultAccount,
798 boolean contactWritableOnly) {
799 final AccountComparator comparator = new AccountComparator(defaultAccount);
800 final List<AccountWithDataSet> accounts = getAccounts(contactWritableOnly);
801 Collections.sort(accounts, comparator);
802 return accounts;
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700803 }
804
805 /**
Chiao Cheng6c712f42012-11-26 15:35:28 -0800806 * Return the list of all known, group writable {@link AccountWithDataSet}'s.
807 */
808 public List<AccountWithDataSet> getGroupWritableAccounts() {
809 ensureAccountsLoaded();
Gary Maiac333592016-09-28 17:27:40 -0700810 return Lists.newArrayList(mGroupWritableAccounts);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800811 }
812
813 /**
Wenyi Wang56c8a0c2016-09-30 11:11:10 -0700814 * Returns the default google account specified in preferences, the first google account
815 * if it is not specified in preferences or is no longer on the device, and null otherwise.
816 */
817 @Override
818 public Account getDefaultGoogleAccount() {
819 final AccountManager accountManager = AccountManager.get(mContext);
820 final SharedPreferences sharedPreferences =
821 mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE);
822 final String defaultAccountKey =
823 mContext.getResources().getString(R.string.contact_editor_default_account_key);
824 return getDefaultGoogleAccount(accountManager, sharedPreferences, defaultAccountKey);
825 }
826
827 /**
Chiao Cheng6c712f42012-11-26 15:35:28 -0800828 * Find the best {@link DataKind} matching the requested
829 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
830 * If no direct match found, we try searching {@link FallbackAccountType}.
831 */
832 @Override
833 public DataKind getKindOrFallback(AccountType type, String mimeType) {
834 ensureAccountsLoaded();
835 DataKind kind = null;
836
837 // Try finding account type and kind matching request
838 if (type != null) {
839 kind = type.getKindForMimetype(mimeType);
840 }
841
842 if (kind == null) {
843 // Nothing found, so try fallback as last resort
844 kind = mFallbackAccountType.getKindForMimetype(mimeType);
845 }
846
847 if (kind == null) {
Chiao Cheng5df73762012-12-12 11:28:35 -0800848 if (Log.isLoggable(TAG, Log.DEBUG)) {
849 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
850 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800851 }
852
853 return kind;
854 }
855
856 /**
857 * Return {@link AccountType} for the given account type and data set.
858 */
859 @Override
860 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
861 ensureAccountsLoaded();
862 synchronized (this) {
863 AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
864 return type != null ? type : mFallbackAccountType;
865 }
866 }
867
868 /**
869 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
870 * which support the "invite" feature and have one or more account. This is an unfiltered
871 * list. See {@link #getUsableInvitableAccountTypes()}.
872 */
873 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
874 ensureAccountsLoaded();
875 return mInvitableAccountTypes;
876 }
877
878 @Override
879 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
880 ensureAccountsLoaded();
881 // Since this method is not thread-safe, it's possible for multiple threads to encounter
882 // the situation where (1) the cache has not been initialized yet or
883 // (2) an async task to refresh the account type list in the cache has already been
884 // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
885 // while we compute the actual result in the background. We use this approach instead of
886 // using "synchronized" because computing the account type list involves a DB read, and
887 // can potentially cause a deadlock situation if this method is called from code which
888 // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
889 // account types for a short period of time seems more manageable than enforcing the
890 // context in which this method is called.
891
892 // Computing the list of usable invitable account types is done on the fly as requested.
893 // If this method has never been called before, then block until the list has been computed.
894 if (!mInvitablesCacheIsInitialized.get()) {
895 mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
896 mInvitablesCacheIsInitialized.set(true);
897 } else {
898 // Otherwise, there is a value in the cache. If the value has expired and
899 // an async task has not already been started by another thread, then kick off a new
900 // async task to compute the list.
901 if (mInvitableAccountTypeCache.isExpired() &&
902 mInvitablesTaskIsRunning.compareAndSet(false, true)) {
903 new FindInvitablesTask().execute();
904 }
905 }
906
907 return mInvitableAccountTypeCache.getCachedValue();
908 }
909
910 /**
911 * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
912 * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
913 */
914 @VisibleForTesting
915 static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context,
916 Collection<AccountWithDataSet> accounts,
917 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
918 HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
919 for (AccountWithDataSet account : accounts) {
920 AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
921 AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
922 if (type == null) continue; // just in case
923 if (result.containsKey(accountTypeWithDataSet)) continue;
924
925 if (Log.isLoggable(TAG, Log.DEBUG)) {
926 Log.d(TAG, "Type " + accountTypeWithDataSet
927 + " inviteClass=" + type.getInviteContactActivityClassName());
928 }
929 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
930 result.put(accountTypeWithDataSet, type);
931 }
932 }
933 return Collections.unmodifiableMap(result);
934 }
935
936 /**
937 * Return all usable {@link AccountType}s that support the "invite" feature from the
938 * list of all potential invitable account types (retrieved from
939 * {@link #getAllInvitableAccountTypes}). A usable invitable account type means:
940 * (1) there is at least 1 raw contact in the database with that account type, and
941 * (2) the app contributing the account type is not disabled.
942 *
943 * Warning: Don't use on the UI thread because this can scan the database.
944 */
945 private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
946 Context context) {
947 Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
948 if (allInvitables.isEmpty()) {
949 return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
950 }
951
952 final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
953 result.putAll(allInvitables);
954
955 final PackageManager packageManager = context.getPackageManager();
956 for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
957 AccountType accountType = allInvitables.get(accountTypeWithDataSet);
958
959 // Make sure that account types don't come from apps that are disabled.
960 Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType,
961 SAMPLE_CONTACT_URI);
962 if (invitableIntent == null) {
963 result.remove(accountTypeWithDataSet);
964 continue;
965 }
966 ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent,
967 PackageManager.MATCH_DEFAULT_ONLY);
968 if (resolveInfo == null) {
969 // If we can't find an activity to start for this intent, then there's no point in
970 // showing this option to the user.
971 result.remove(accountTypeWithDataSet);
972 continue;
973 }
974
975 // Make sure that there is at least 1 raw contact with this account type. This check
976 // is non-trivial and should not be done on the UI thread.
977 if (!accountTypeWithDataSet.hasData(context)) {
978 result.remove(accountTypeWithDataSet);
979 }
980 }
981
982 return Collections.unmodifiableMap(result);
983 }
984
985 @Override
986 public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
987 ensureAccountsLoaded();
988 final List<AccountType> accountTypes = Lists.newArrayList();
989 synchronized (this) {
990 for (AccountType type : mAccountTypesWithDataSets.values()) {
991 if (!contactWritableOnly || type.areContactsWritable()) {
992 accountTypes.add(type);
993 }
994 }
995 }
996 return accountTypes;
997 }
998
999 /**
1000 * Background task to find all usable {@link AccountType}s that support the "invite" feature
1001 * from the list of all potential invitable account types. Once the work is completed,
1002 * the list of account types is stored in the {@link AccountTypeManager}'s
1003 * {@link InvitableAccountTypeCache}.
1004 */
1005 private class FindInvitablesTask extends AsyncTask<Void, Void,
1006 Map<AccountTypeWithDataSet, AccountType>> {
1007
1008 @Override
1009 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
1010 return findUsableInvitableAccountTypes(mContext);
1011 }
1012
1013 @Override
1014 protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
1015 mInvitableAccountTypeCache.setCachedValue(accountTypes);
1016 mInvitablesTaskIsRunning.set(false);
1017 }
1018 }
1019
1020 /**
1021 * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a
1022 * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only
1023 * for {@link #TIME_TO_LIVE} milliseconds.
1024 */
1025 private static final class InvitableAccountTypeCache {
1026
1027 /**
1028 * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds
1029 * has elapsed.
1030 */
1031 private static final long TIME_TO_LIVE = 60000;
1032
1033 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
1034
1035 private long mTimeLastSet;
1036
1037 /**
1038 * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
1039 * otherwise.
1040 */
1041 public boolean isExpired() {
1042 return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
1043 }
1044
1045 /**
1046 * Returns the cached value. Note that the caller is responsible for checking
1047 * {@link #isExpired()} to ensure that the value is not stale.
1048 */
1049 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
1050 return mInvitableAccountTypes;
1051 }
1052
1053 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
1054 mInvitableAccountTypes = map;
1055 mTimeLastSet = SystemClock.elapsedRealtime();
1056 }
1057 }
1058}