summaryrefslogtreecommitdiff
path: root/java/com/android/contacts/common/model/AccountTypeManager.java
blob: f225ff6ac6bb2660602c0fb8ce2abf78e20a3d6e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common.model;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.accounts.OnAccountsUpdateListener;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SyncAdapterType;
import android.content.SyncStatusObserver;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.provider.ContactsContract;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TimingLogger;
import com.android.contacts.common.MoreContactUtils;
import com.android.contacts.common.list.ContactListFilterController;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountTypeWithDataSet;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.model.account.ExchangeAccountType;
import com.android.contacts.common.model.account.ExternalAccountType;
import com.android.contacts.common.model.account.FallbackAccountType;
import com.android.contacts.common.model.account.GoogleAccountType;
import com.android.contacts.common.model.account.SamsungAccountType;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.util.Constants;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Singleton holder for all parsed {@link AccountType} available on the system, typically filled
 * through {@link PackageManager} queries.
 */
public abstract class AccountTypeManager {

  static final String TAG = "AccountTypeManager";

  private static final Object mInitializationLock = new Object();
  private static AccountTypeManager mAccountTypeManager;

  /**
   * Requests the singleton instance of {@link AccountTypeManager} with data bound from the
   * available authenticators. This method can safely be called from the UI thread.
   */
  public static AccountTypeManager getInstance(Context context) {
    synchronized (mInitializationLock) {
      if (mAccountTypeManager == null) {
        context = context.getApplicationContext();
        mAccountTypeManager = new AccountTypeManagerImpl(context);
      }
    }
    return mAccountTypeManager;
  }

  /**
   * Set the instance of account type manager. This is only for and should only be used by unit
   * tests. While having this method is not ideal, it's simpler than the alternative of holding this
   * as a service in the ContactsApplication context class.
   *
   * @param mockManager The mock AccountTypeManager.
   */
  public static void setInstanceForTest(AccountTypeManager mockManager) {
    synchronized (mInitializationLock) {
      mAccountTypeManager = mockManager;
    }
  }

  /**
   * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact
   * writable accounts (if contactWritableOnly is true).
   */
  // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
  public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);

  /** Returns the list of accounts that are group writable. */
  public abstract List<AccountWithDataSet> getGroupWritableAccounts();

  public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);

  public final AccountType getAccountType(String accountType, String dataSet) {
    return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
  }

  public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
    if (account != null) {
      return getAccountType(account.getAccountTypeWithDataSet());
    }
    return getAccountType(null, null);
  }

  /**
   * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
   *     support the "invite" feature and have one or more account.
   *     <p>This is a filtered down and more "usable" list compared to {@link
   *     #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app
   *     that contributed the account type is not disabled (in order to avoid presenting the user
   *     with an option that does nothing), and (2) that there is at least one raw contact with that
   *     account type in the database (assuming that the user probably doesn't use that account
   *     type).
   *     <p>Warning: Don't use on the UI thread because this can scan the database.
   */
  public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();

  /**
   * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
   * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
   * {@link FallbackAccountType}.
   */
  public DataKind getKindOrFallback(AccountType type, String mimeType) {
    return type == null ? null : type.getKindForMimetype(mimeType);
  }

  /**
   * Returns all registered {@link AccountType}s, including extension ones.
   *
   * @param contactWritableOnly if true, it only returns ones that support writing contacts.
   */
  public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);

  /**
   * @param contactWritableOnly if true, it only returns ones that support writing contacts.
   * @return true when this instance contains the given account.
   */
  public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
    for (AccountWithDataSet account_2 : getAccounts(false)) {
      if (account.equals(account_2)) {
        return true;
      }
    }
    return false;
  }
}

class AccountTypeManagerImpl extends AccountTypeManager
    implements OnAccountsUpdateListener, SyncStatusObserver {

  private static final Map<AccountTypeWithDataSet, AccountType>
      EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
          Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());

  /**
   * A sample contact URI used to test whether any activities will respond to an invitable intent
   * with the given URI as the intent data. This doesn't need to be specific to a real contact
   * because an app that intercepts the intent should probably do so for all types of contact URIs.
   */
  private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx");

  private static final int MESSAGE_LOAD_DATA = 0;
  private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
  private static final Comparator<AccountWithDataSet> ACCOUNT_COMPARATOR =
      new Comparator<AccountWithDataSet>() {
        @Override
        public int compare(AccountWithDataSet a, AccountWithDataSet b) {
          if (Objects.equals(a.name, b.name)
              && Objects.equals(a.type, b.type)
              && Objects.equals(a.dataSet, b.dataSet)) {
            return 0;
          } else if (b.name == null || b.type == null) {
            return -1;
          } else if (a.name == null || a.type == null) {
            return 1;
          } else {
            int diff = a.name.compareTo(b.name);
            if (diff != 0) {
              return diff;
            }
            diff = a.type.compareTo(b.type);
            if (diff != 0) {
              return diff;
            }

            // Accounts without data sets get sorted before those that have them.
            if (a.dataSet != null) {
              return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet);
            } else {
              return -1;
            }
          }
        }
      };
  private final InvitableAccountTypeCache mInvitableAccountTypeCache;
  /**
   * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
   * initialized. False otherwise.
   */
  private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
  /**
   * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False
   * otherwise.
   */
  private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);

  private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
  private Context mContext;
  private final Runnable mCheckFilterValidityRunnable =
      new Runnable() {
        @Override
        public void run() {
          ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
        }
      };
  private AccountManager mAccountManager;
  private AccountType mFallbackAccountType;
  private List<AccountWithDataSet> mAccounts = new ArrayList<>();
  private List<AccountWithDataSet> mContactWritableAccounts = new ArrayList<>();
  private List<AccountWithDataSet> mGroupWritableAccounts = new ArrayList<>();
  private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = new ArrayMap<>();
  private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
      EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
  private HandlerThread mListenerThread;
  private Handler mListenerHandler;
  private BroadcastReceiver mBroadcastReceiver =
      new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
          Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
          mListenerHandler.sendMessage(msg);
        }
      };
  /* A latch that ensures that asynchronous initialization completes before data is used */
  private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);

  /** Internal constructor that only performs initial parsing. */
  public AccountTypeManagerImpl(Context context) {
    mContext = context;
    mFallbackAccountType = new FallbackAccountType(context);

    mAccountManager = AccountManager.get(mContext);

    mListenerThread = new HandlerThread("AccountChangeListener");
    mListenerThread.start();
    mListenerHandler =
        new Handler(mListenerThread.getLooper()) {
          @Override
          public void handleMessage(Message msg) {
            switch (msg.what) {
              case MESSAGE_LOAD_DATA:
                loadAccountsInBackground();
                break;
              case MESSAGE_PROCESS_BROADCAST_INTENT:
                processBroadcastIntent((Intent) msg.obj);
                break;
            }
          }
        };

    mInvitableAccountTypeCache = new InvitableAccountTypeCache();

    // Request updates when packages or accounts change
    IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
    filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
    filter.addDataScheme("package");
    mContext.registerReceiver(mBroadcastReceiver, filter);
    IntentFilter sdFilter = new IntentFilter();
    sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
    sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
    mContext.registerReceiver(mBroadcastReceiver, sdFilter);

    // Request updates when locale is changed so that the order of each field will
    // be able to be changed on the locale change.
    filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
    mContext.registerReceiver(mBroadcastReceiver, filter);

    mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);

    ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);

    mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
  }

  /**
   * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given
   * account type.
   */
  protected static AuthenticatorDescription findAuthenticator(
      AuthenticatorDescription[] auths, String accountType) {
    for (AuthenticatorDescription auth : auths) {
      if (accountType.equals(auth.type)) {
        return auth;
      }
    }
    return null;
  }

  /**
   * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its
   * {@link AccountType#getInviteContactActivityClassName()} is not empty.
   */
  @VisibleForTesting
  static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(
      Context context,
      Collection<AccountWithDataSet> accounts,
      Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
    Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
    for (AccountWithDataSet account : accounts) {
      AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
      AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
      if (type == null) {
        continue; // just in case
      }
      if (result.containsKey(accountTypeWithDataSet)) {
        continue;
      }

      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(
            TAG,
            "Type "
                + accountTypeWithDataSet
                + " inviteClass="
                + type.getInviteContactActivityClassName());
      }
      if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
        result.put(accountTypeWithDataSet, type);
      }
    }
    return Collections.unmodifiableMap(result);
  }

  @Override
  public void onStatusChanged(int which) {
    mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
  }

  public void processBroadcastIntent(Intent intent) {
    mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
  }

  /* This notification will arrive on the background thread */
  public void onAccountsUpdated(Account[] accounts) {
    // Refresh to catch any changed accounts
    loadAccountsInBackground();
  }

  /**
   * Returns instantly if accounts and account types have already been loaded. Otherwise waits for
   * the background thread to complete the loading.
   */
  void ensureAccountsLoaded() {
    CountDownLatch latch = mInitializationLatch;
    if (latch == null) {
      return;
    }
    while (true) {
      try {
        latch.await();
        return;
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    }
  }

  /**
   * Loads account list and corresponding account types (potentially with data sets). Always called
   * on a background thread.
   */
  protected void loadAccountsInBackground() {
    if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
      Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
    }
    TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
    final long startTime = SystemClock.currentThreadTimeMillis();
    final long startTimeWall = SystemClock.elapsedRealtime();

    // Account types, keyed off the account type and data set concatenation.
    final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = new ArrayMap<>();

    // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}.  Since there can
    // be multiple account types (with different data sets) for the same type of account, each
    // type string may have multiple AccountType entries.
    final Map<String, List<AccountType>> accountTypesByType = new ArrayMap<>();

    final List<AccountWithDataSet> allAccounts = new ArrayList<>();
    final List<AccountWithDataSet> contactWritableAccounts = new ArrayList<>();
    final List<AccountWithDataSet> groupWritableAccounts = new ArrayList<>();
    final Set<String> extensionPackages = new HashSet<>();

    final AccountManager am = mAccountManager;

    final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
    final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();

    // First process sync adapters to find any that provide contact data.
    for (SyncAdapterType sync : syncs) {
      if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
        // Skip sync adapters that don't provide contact data.
        continue;
      }

      // Look for the formatting details provided by each sync
      // adapter, using the authenticator to find general resources.
      final String type = sync.accountType;
      final AuthenticatorDescription auth = findAuthenticator(auths, type);
      if (auth == null) {
        Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
        continue;
      }

      AccountType accountType;
      if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
        accountType = new GoogleAccountType(mContext, auth.packageName);
      } else if (ExchangeAccountType.isExchangeType(type)) {
        accountType = new ExchangeAccountType(mContext, auth.packageName, type);
      } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) {
        accountType = new SamsungAccountType(mContext, auth.packageName, type);
      } else {
        Log.d(
            TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName);
        accountType = new ExternalAccountType(mContext, auth.packageName, false);
      }
      if (!accountType.isInitialized()) {
        if (accountType.isEmbedded()) {
          throw new IllegalStateException(
              "Problem initializing embedded type " + accountType.getClass().getCanonicalName());
        } else {
          // Skip external account types that couldn't be initialized.
          continue;
        }
      }

      accountType.accountType = auth.type;
      accountType.titleRes = auth.labelId;
      accountType.iconRes = auth.iconId;

      addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);

      // Check to see if the account type knows of any other non-sync-adapter packages
      // that may provide other data sets of contact data.
      extensionPackages.addAll(accountType.getExtensionPackageNames());
    }

    // If any extension packages were specified, process them as well.
    if (!extensionPackages.isEmpty()) {
      Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
      for (String extensionPackage : extensionPackages) {
        ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true);
        if (!accountType.isInitialized()) {
          // Skip external account types that couldn't be initialized.
          continue;
        }
        if (!accountType.hasContactsMetadata()) {
          Log.w(
              TAG,
              "Skipping extension package "
                  + extensionPackage
                  + " because"
                  + " it doesn't have the CONTACTS_STRUCTURE metadata");
          continue;
        }
        if (TextUtils.isEmpty(accountType.accountType)) {
          Log.w(
              TAG,
              "Skipping extension package "
                  + extensionPackage
                  + " because"
                  + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
                  + " attribute");
          continue;
        }
        Log.d(
            TAG,
            "Registering extension package account type="
                + accountType.accountType
                + ", dataSet="
                + accountType.dataSet
                + ", packageName="
                + extensionPackage);

        addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
      }
    }
    timings.addSplit("Loaded account types");

    // Map in accounts to associate the account names with each account type entry.
    Account[] accounts = mAccountManager.getAccounts();
    for (Account account : accounts) {
      boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;

      if (syncable) {
        List<AccountType> accountTypes = accountTypesByType.get(account.type);
        if (accountTypes != null) {
          // Add an account-with-data-set entry for each account type that is
          // authenticated by this account.
          for (AccountType accountType : accountTypes) {
            AccountWithDataSet accountWithDataSet =
                new AccountWithDataSet(account.name, account.type, accountType.dataSet);
            allAccounts.add(accountWithDataSet);
            if (accountType.areContactsWritable()) {
              contactWritableAccounts.add(accountWithDataSet);
            }
            if (accountType.isGroupMembershipEditable()) {
              groupWritableAccounts.add(accountWithDataSet);
            }
          }
        }
      }
    }

    Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
    Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR);
    Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR);

    timings.addSplit("Loaded accounts");

    synchronized (this) {
      mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
      mAccounts = allAccounts;
      mContactWritableAccounts = contactWritableAccounts;
      mGroupWritableAccounts = groupWritableAccounts;
      mInvitableAccountTypes =
          findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet);
    }

    timings.dumpToLog();
    final long endTimeWall = SystemClock.elapsedRealtime();
    final long endTime = SystemClock.currentThreadTimeMillis();

    Log.i(
        TAG,
        "Loaded meta-data for "
            + mAccountTypesWithDataSets.size()
            + " account types, "
            + mAccounts.size()
            + " accounts in "
            + (endTimeWall - startTimeWall)
            + "ms(wall) "
            + (endTime - startTime)
            + "ms(cpu)");

    if (mInitializationLatch != null) {
      mInitializationLatch.countDown();
      mInitializationLatch = null;
    }
    if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
      Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
    }

    // Check filter validity since filter may become obsolete after account update. It must be
    // done from UI thread.
    mMainThreadHandler.post(mCheckFilterValidityRunnable);
  }

  // Bookkeeping method for tracking the known account types in the given maps.
  private void addAccountType(
      AccountType accountType,
      Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
      Map<String, List<AccountType>> accountTypesByType) {
    accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
    List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
    if (accountsForType == null) {
      accountsForType = new ArrayList<>();
    }
    accountsForType.add(accountType);
    accountTypesByType.put(accountType.accountType, accountsForType);
  }

  /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */
  @Override
  public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
    ensureAccountsLoaded();
    return contactWritableOnly ? mContactWritableAccounts : mAccounts;
  }

  /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */
  public List<AccountWithDataSet> getGroupWritableAccounts() {
    ensureAccountsLoaded();
    return mGroupWritableAccounts;
  }

  /**
   * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
   * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
   * {@link FallbackAccountType}.
   */
  @Override
  public DataKind getKindOrFallback(AccountType type, String mimeType) {
    ensureAccountsLoaded();
    DataKind kind = null;

    // Try finding account type and kind matching request
    if (type != null) {
      kind = type.getKindForMimetype(mimeType);
    }

    if (kind == null) {
      // Nothing found, so try fallback as last resort
      kind = mFallbackAccountType.getKindForMimetype(mimeType);
    }

    if (kind == null) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
      }
    }

    return kind;
  }

  /** Return {@link AccountType} for the given account type and data set. */
  @Override
  public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
    ensureAccountsLoaded();
    synchronized (this) {
      AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
      return type != null ? type : mFallbackAccountType;
    }
  }

  /**
   * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
   *     support the "invite" feature and have one or more account. This is an unfiltered list. See
   *     {@link #getUsableInvitableAccountTypes()}.
   */
  private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
    ensureAccountsLoaded();
    return mInvitableAccountTypes;
  }

  @Override
  public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
    ensureAccountsLoaded();
    // Since this method is not thread-safe, it's possible for multiple threads to encounter
    // the situation where (1) the cache has not been initialized yet or
    // (2) an async task to refresh the account type list in the cache has already been
    // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
    // while we compute the actual result in the background. We use this approach instead of
    // using "synchronized" because computing the account type list involves a DB read, and
    // can potentially cause a deadlock situation if this method is called from code which
    // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
    // account types for a short period of time seems more manageable than enforcing the
    // context in which this method is called.

    // Computing the list of usable invitable account types is done on the fly as requested.
    // If this method has never been called before, then block until the list has been computed.
    if (!mInvitablesCacheIsInitialized.get()) {
      mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
      mInvitablesCacheIsInitialized.set(true);
    } else {
      // Otherwise, there is a value in the cache. If the value has expired and
      // an async task has not already been started by another thread, then kick off a new
      // async task to compute the list.
      if (mInvitableAccountTypeCache.isExpired()
          && mInvitablesTaskIsRunning.compareAndSet(false, true)) {
        new FindInvitablesTask().execute();
      }
    }

    return mInvitableAccountTypeCache.getCachedValue();
  }

  /**
   * Return all usable {@link AccountType}s that support the "invite" feature from the list of all
   * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A
   * usable invitable account type means: (1) there is at least 1 raw contact in the database with
   * that account type, and (2) the app contributing the account type is not disabled.
   *
   * <p>Warning: Don't use on the UI thread because this can scan the database.
   */
  private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
      Context context) {
    Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
    if (allInvitables.isEmpty()) {
      return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
    }

    final Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
    result.putAll(allInvitables);

    final PackageManager packageManager = context.getPackageManager();
    for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
      AccountType accountType = allInvitables.get(accountTypeWithDataSet);

      // Make sure that account types don't come from apps that are disabled.
      Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI);
      if (invitableIntent == null) {
        result.remove(accountTypeWithDataSet);
        continue;
      }
      ResolveInfo resolveInfo =
          packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY);
      if (resolveInfo == null) {
        // If we can't find an activity to start for this intent, then there's no point in
        // showing this option to the user.
        result.remove(accountTypeWithDataSet);
        continue;
      }

      // Make sure that there is at least 1 raw contact with this account type. This check
      // is non-trivial and should not be done on the UI thread.
      if (!accountTypeWithDataSet.hasData(context)) {
        result.remove(accountTypeWithDataSet);
      }
    }

    return Collections.unmodifiableMap(result);
  }

  @Override
  public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
    ensureAccountsLoaded();
    final List<AccountType> accountTypes = new ArrayList<>();
    synchronized (this) {
      for (AccountType type : mAccountTypesWithDataSets.values()) {
        if (!contactWritableOnly || type.areContactsWritable()) {
          accountTypes.add(type);
        }
      }
    }
    return accountTypes;
  }

  /**
   * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link
   * Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only for {@link
   * #TIME_TO_LIVE} milliseconds.
   */
  private static final class InvitableAccountTypeCache {

    /**
     * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has
     * elapsed.
     */
    private static final long TIME_TO_LIVE = 60000;

    private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;

    private long mTimeLastSet;

    /**
     * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
     * otherwise.
     */
    public boolean isExpired() {
      return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
    }

    /**
     * Returns the cached value. Note that the caller is responsible for checking {@link
     * #isExpired()} to ensure that the value is not stale.
     */
    public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
      return mInvitableAccountTypes;
    }

    public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
      mInvitableAccountTypes = map;
      mTimeLastSet = SystemClock.elapsedRealtime();
    }
  }

  /**
   * Background task to find all usable {@link AccountType}s that support the "invite" feature from
   * the list of all potential invitable account types. Once the work is completed, the list of
   * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}.
   */
  private class FindInvitablesTask
      extends AsyncTask<Void, Void, Map<AccountTypeWithDataSet, AccountType>> {

    @Override
    protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
      return findUsableInvitableAccountTypes(mContext);
    }

    @Override
    protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
      mInvitableAccountTypeCache.setCachedValue(accountTypes);
      mInvitablesTaskIsRunning.set(false);
    }
  }
}