Merge changes Ib4c2a049,I1ec162e0,Iaf0b73b9,Ie6545785,I9ab87cde into ub-contactsdialer-i-dev

* changes:
  Make account filters for nav drawer update
  Replace AccountTypeManager usages in PeopleActivity
  Include device local accounts in AccountTypeManager
  Load accounts in background for SIM import
  Query for accounts on-demand in AccountTypeManager
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index 5f4d181..8d8e2db 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -18,13 +18,15 @@
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
-import android.content.AsyncTaskLoader;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.IntentFilter;
 import android.content.Loader;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.widget.Snackbar;
+import android.support.v4.content.LocalBroadcastManager;
 import android.support.v4.util.ArrayMap;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.ContentLoadingProgressBar;
@@ -47,6 +49,11 @@
 import com.android.contacts.model.SimContact;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferences;
+import com.android.contacts.util.concurrent.ContactsExecutors;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -54,6 +61,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 
 /**
  * Dialog that presents a list of contacts from a SIM card that can be imported into a selected
@@ -77,6 +85,8 @@
     private ListView mListView;
     private View mImportButton;
 
+    private Bundle mSavedInstanceState;
+
     private final Map<AccountWithDataSet, long[]> mPerAccountCheckedIds = new ArrayMap<>();
 
     private int mSubscriptionId;
@@ -85,6 +95,7 @@
     public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        mSavedInstanceState = savedInstanceState;
         mPreferences = new ContactsPreferences(getContext());
         mAccountTypeManager = AccountTypeManager.getInstance(getActivity());
         mAdapter = new SimContactAdapter(getActivity());
@@ -128,7 +139,6 @@
             }
         });
         mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount());
-        restoreAdapterSelectedStates(savedInstanceState);
 
         mListView = (ListView) view.findViewById(R.id.list);
         mListView.setOnScrollListener(this);
@@ -224,7 +234,7 @@
     }
 
     @Override
-    public SimContactLoader onCreateLoader(int id, Bundle args) {
+    public Loader<LoaderResult> onCreateLoader(int id, Bundle args) {
         return new SimContactLoader(getContext(), mSubscriptionId);
     }
 
@@ -235,6 +245,8 @@
         if (data == null) {
             return;
         }
+        mAccountHeaderPresenter.setAccounts(data.accounts);
+        restoreAdapterSelectedStates(data.accounts);
         mAdapter.setData(data);
         mListView.setEmptyView(getView().findViewById(R.id.empty_message));
 
@@ -246,17 +258,17 @@
     public void onLoaderReset(Loader<LoaderResult> loader) {
     }
 
-    private void restoreAdapterSelectedStates(Bundle savedInstanceState) {
-        if (savedInstanceState == null) {
+    private void restoreAdapterSelectedStates(List<AccountWithDataSet> accounts) {
+        if (mSavedInstanceState == null) {
             return;
         }
 
-        final List<AccountWithDataSet> accounts = mAccountTypeManager.getAccounts(true);
         for (AccountWithDataSet account : accounts) {
-            final long[] selections = savedInstanceState.getLongArray(
+            final long[] selections = mSavedInstanceState.getLongArray(
                     account.stringify() + KEY_SUFFIX_SELECTED_IDS);
             mPerAccountCheckedIds.put(account, selections);
         }
+        mSavedInstanceState = null;
     }
 
     private void saveAdapterSelectedStates(Bundle outState) {
@@ -429,34 +441,44 @@
     }
 
 
-    private static class SimContactLoader extends AsyncTaskLoader<LoaderResult> {
+    private static class SimContactLoader extends ListenableFutureLoader<LoaderResult> {
         private SimContactDao mDao;
+        private AccountTypeManager mAccountTypeManager;
         private final int mSubscriptionId;
-        LoaderResult mResult;
+
+        private BroadcastReceiver mReceiver;
 
         public SimContactLoader(Context context, int subscriptionId) {
             super(context);
             mDao = SimContactDao.create(context);
+            mAccountTypeManager = AccountTypeManager.getInstance(getContext());
             mSubscriptionId = subscriptionId;
         }
 
         @Override
-        protected void onStartLoading() {
-            if (mResult != null) {
-                deliverResult(mResult);
-            } else {
-                forceLoad();
-            }
+        protected ListenableFuture<LoaderResult> loadData() {
+            final ListenableFuture<List<Object>> future = Futures.<Object>allAsList(
+                    mAccountTypeManager
+                            .filterAccountsByTypeAsync(AccountTypeManager.writableFilter()),
+                    ContactsExecutors.getSimReadExecutor().<Object>submit(
+                            new Callable<Object>() {
+                        @Override
+                        public LoaderResult call() throws Exception {
+                            return loadFromSim();
+                        }
+                    }));
+            return Futures.transform(future, new Function<List<Object>, LoaderResult>() {
+                @Override
+                public LoaderResult apply(List<Object> input) {
+                    final List<AccountWithDataSet> accounts = (List<AccountWithDataSet>) input.get(0);
+                    final LoaderResult simLoadResult = (LoaderResult) input.get(1);
+                    simLoadResult.accounts = accounts;
+                    return simLoadResult;
+                }
+            });
         }
 
-        @Override
-        public void deliverResult(LoaderResult result) {
-            mResult = result;
-            super.deliverResult(result);
-        }
-
-        @Override
-        public LoaderResult loadInBackground() {
+        private LoaderResult loadFromSim() {
             final SimCard sim = mDao.getSimBySubscriptionId(mSubscriptionId);
             LoaderResult result = new LoaderResult();
             if (sim == null) {
@@ -470,13 +492,24 @@
         }
 
         @Override
-        protected void onReset() {
-            mResult = null;
+        protected void onStartLoading() {
+            super.onStartLoading();
+            if (mReceiver == null) {
+                mReceiver = new ForceLoadReceiver();
+                LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
+                        new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+            }
         }
 
+        @Override
+        protected void onReset() {
+            super.onReset();
+            LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
+        }
     }
 
     public static class LoaderResult {
+        public List<AccountWithDataSet> accounts;
         public ArrayList<SimContact> contacts;
         public Map<AccountWithDataSet, Set<SimContact>> accountsMap;
     }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 113121b..426dcd7 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -68,6 +68,7 @@
 import com.android.contacts.logging.Logger;
 import com.android.contacts.logging.ScreenEvent.ScreenType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountFilterUtil;
 import com.android.contacts.util.Constants;
@@ -77,6 +78,7 @@
 import com.android.contactsbind.FeatureHighlightHelper;
 import com.android.contactsbind.ObjectFactory;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -100,6 +102,7 @@
 
     private ContactsIntentResolver mIntentResolver;
     private ContactsRequest mRequest;
+    private AccountTypeManager mAccountTypeManager;
 
     private FloatingActionButtonController mFloatingActionButtonController;
     private View mFloatingActionButtonContainer;
@@ -163,17 +166,18 @@
                 return;
             }
 
-            final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(this)
-                    .getAccounts(/* contactsWritableOnly */ true);
-            final List<Account> syncableAccounts = filter.getSyncableAccounts(accounts);
-            // If one of the accounts is active or pending, use spinning circle to indicate one of
-            // the syncs is in progress.
-            if (syncableAccounts != null && syncableAccounts.size() > 0) {
-                for (Account account: syncableAccounts) {
-                    if (SyncUtil.isSyncStatusPendingOrActive(account)) {
-                        return;
-                    }
-                }
+            final List<AccountWithDataSet> accounts;
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT &&
+                    filter.isGoogleAccountType()) {
+                accounts = Collections.singletonList(new AccountWithDataSet(filter.accountName,
+                        filter.accountType, null));
+            } else if (filter.shouldShowSyncState()) {
+                accounts = mAccountTypeManager.getWritableGoogleAccounts();
+            } else {
+                accounts = Collections.emptyList();
+            }
+            if (SyncUtil.isAnySyncing(accounts)) {
+                return;
             }
             swipeRefreshLayout.setRefreshing(false);
         }
@@ -222,6 +226,7 @@
             Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
         }
         super.onCreate(savedState);
+        mAccountTypeManager = AccountTypeManager.getInstance(this);
 
         if (RequestPermissionsActivity.startPermissionActivity(this)) {
             return;
@@ -573,21 +578,9 @@
 
     private boolean shouldShowList() {
         return mProviderStatus != null
-                && ((mProviderStatus.equals(ProviderStatus.STATUS_EMPTY) && hasNonLocalAccount())
-                        || mProviderStatus.equals(ProviderStatus.STATUS_NORMAL));
-    }
-
-    // Returns true if there are real accounts (not "local" account) in the list of accounts.
-    private boolean hasNonLocalAccount() {
-        final List<AccountWithDataSet> allAccounts =
-                AccountTypeManager.getInstance(this).getAccounts(/* contactWritableOnly */ false);
-        if (allAccounts == null || allAccounts.size() == 0) {
-            return false;
-        }
-        if (allAccounts.size() > 1) {
-            return true;
-        }
-        return !allAccounts.get(0).isNullAccount();
+                && ((mProviderStatus.equals(ProviderStatus.STATUS_EMPTY)
+                && mAccountTypeManager.hasNonLocalAccount())
+                || mProviderStatus.equals(ProviderStatus.STATUS_NORMAL));
     }
 
     private void invalidateOptionsMenuIfNeeded() {
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
index c94dcd4..0185419 100644
--- a/src/com/android/contacts/editor/AccountHeaderPresenter.java
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -58,6 +58,7 @@
     private final Context mContext;
     private AccountDisplayInfoFactory mAccountDisplayInfoFactory;
 
+    private List<AccountWithDataSet> mAccounts;
     private AccountWithDataSet mCurrentAccount;
 
     // Account header
@@ -82,8 +83,6 @@
         mAccountHeaderName = (TextView) container.findViewById(R.id.account_name);
         mAccountHeaderIcon = (ImageView) container.findViewById(R.id.account_type_icon);
         mAccountHeaderExpanderIcon = (ImageView) container.findViewById(R.id.account_expander_icon);
-
-        mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(mContext);
     }
 
     public void setObserver(Observer observer) {
@@ -101,6 +100,17 @@
         updateDisplayedAccount();
     }
 
+    public void setAccounts(List<AccountWithDataSet> accounts) {
+        mAccounts = accounts;
+        mAccountDisplayInfoFactory = new AccountDisplayInfoFactory(mContext, accounts);
+        // If the current account was removed just switch to the next one in the list.
+        if (mCurrentAccount != null && !mAccounts.contains(mCurrentAccount)) {
+            mCurrentAccount = mAccounts.isEmpty() ? null : accounts.get(0);
+            mObserver.onChange(this);
+        }
+        updateDisplayedAccount();
+    }
+
     public AccountWithDataSet getCurrentAccount() {
         return mCurrentAccount;
     }
@@ -120,17 +130,14 @@
     private void updateDisplayedAccount() {
         mAccountHeaderContainer.setVisibility(View.GONE);
         if (mCurrentAccount == null) return;
+        if (mAccounts == null) return;
 
         final AccountDisplayInfo account =
                 mAccountDisplayInfoFactory.getAccountDisplayInfo(mCurrentAccount);
 
         final String accountLabel = getAccountLabel(account);
 
-        // Either the account header or selector should be shown, not both.
-        final List<AccountWithDataSet> accounts =
-                AccountTypeManager.getInstance(mContext).getAccounts(true);
-
-        if (accounts.size() > 1) {
+        if (mAccounts.size() > 1) {
             addAccountSelector(accountLabel);
         } else {
             addAccountHeader(accountLabel);
@@ -174,9 +181,7 @@
     private void showPopup() {
         final ListPopupWindow popup = new ListPopupWindow(mContext);
         final AccountsListAdapter adapter =
-                new AccountsListAdapter(mContext,
-                        AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
-                        mCurrentAccount);
+                new AccountsListAdapter(mContext, mAccounts, mCurrentAccount);
         popup.setWidth(mAccountHeaderContainer.getWidth());
         popup.setAnchorView(mAccountHeaderContainer);
         popup.setAdapter(adapter);
diff --git a/src/com/android/contacts/list/ContactListFilter.java b/src/com/android/contacts/list/ContactListFilter.java
index 850683c..4245be4 100644
--- a/src/com/android/contacts/list/ContactListFilter.java
+++ b/src/com/android/contacts/list/ContactListFilter.java
@@ -420,6 +420,13 @@
         return false;
     }
 
+    public boolean shouldShowSyncState() {
+        return (isGoogleAccountType() && filterType == ContactListFilter.FILTER_TYPE_ACCOUNT)
+                || filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
+                || filterType == ContactListFilter.FILTER_TYPE_CUSTOM
+                || filterType == ContactListFilter.FILTER_TYPE_DEFAULT;
+    }
+
     /**
      * Returns the Google accounts (see {@link #isGoogleAccountType) for this ContactListFilter.
      */
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 24957d7..d87b444 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -18,7 +18,6 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-import android.accounts.AuthenticatorDescription;
 import android.accounts.OnAccountsUpdateListener;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -26,57 +25,48 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
-import android.content.SyncAdapterType;
 import android.content.SyncStatusObserver;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.net.Uri;
 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.v4.content.ContextCompat;
+import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.TimingLogger;
 
 import com.android.contacts.Experiments;
 import com.android.contacts.R;
 import com.android.contacts.list.ContactListFilterController;
+import com.android.contacts.model.account.AccountComparator;
 import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountTypeProvider;
 import com.android.contacts.model.account.AccountTypeWithDataSet;
 import com.android.contacts.model.account.AccountWithDataSet;
-import com.android.contacts.model.account.ExchangeAccountType;
-import com.android.contacts.model.account.ExternalAccountType;
 import com.android.contacts.model.account.FallbackAccountType;
 import com.android.contacts.model.account.GoogleAccountType;
-import com.android.contacts.model.account.SamsungAccountType;
 import com.android.contacts.model.dataitem.DataKind;
-import com.android.contacts.util.Constants;
-import com.android.contacts.util.DeviceLocalAccountTypeFactory;
-import com.android.contactsbind.ObjectFactory;
+import com.android.contacts.util.concurrent.ContactsExecutors;
 import com.android.contactsbind.experiments.Flags;
-import com.google.common.base.Objects;
+import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
 
 import javax.annotation.Nullable;
 
-import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
-
 /**
  * Singleton holder for all parsed {@link AccountType} available on the
  * system, typically filled through {@link PackageManager} queries.
@@ -87,6 +77,9 @@
     private static final Object mInitializationLock = new Object();
     private static AccountTypeManager mAccountTypeManager;
 
+    public static final String BROADCAST_ACCOUNTS_CHANGED = AccountTypeManager.class.getName() +
+            ".AccountsChanged";
+
     /**
      * Requests the singleton instance of {@link AccountTypeManager} with data bound from
      * the available authenticators. This method can safely be called from the UI thread.
@@ -100,8 +93,7 @@
         synchronized (mInitializationLock) {
             if (mAccountTypeManager == null) {
                 context = context.getApplicationContext();
-                mAccountTypeManager = new AccountTypeManagerImpl(context,
-                        ObjectFactory.getDeviceLocalAccountTypeFactory(context));
+                mAccountTypeManager = new AccountTypeManagerImpl(context);
             }
         }
         return mAccountTypeManager;
@@ -121,6 +113,7 @@
     }
 
     private static final AccountTypeManager EMPTY = new AccountTypeManager() {
+
         @Override
         public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
             return Collections.emptyList();
@@ -132,6 +125,17 @@
         }
 
         @Override
+        public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+            return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+        }
+
+        @Override
+        public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+                Predicate<AccountType> type) {
+            return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+        }
+
+        @Override
         public List<AccountWithDataSet> getGroupWritableAccounts() {
             return Collections.emptyList();
         }
@@ -157,6 +161,14 @@
     public abstract List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter);
 
     /**
+     * Loads accounts in background and returns future that will complete with list of all accounts
+     */
+    public abstract ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync();
+
+    public abstract ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+            Predicate<AccountType> type);
+
+    /**
      * Returns the list of accounts that are group writable.
      */
     public abstract List<AccountWithDataSet> getGroupWritableAccounts();
@@ -166,6 +178,39 @@
      */
     public abstract Account getDefaultGoogleAccount();
 
+    /**
+     * Returns the Google Accounts.
+     *
+     * <p>This method exists in addition to filterAccountsByTypeAsync because it should be safe
+     * to call synchronously.
+     * </p>
+     */
+    public List<AccountWithDataSet> getWritableGoogleAccounts() {
+        // This implementation may block and should be overridden by the Impl class
+        return Futures.getUnchecked(filterAccountsByTypeAsync(new Predicate<AccountType>() {
+            @Override
+            public boolean apply(@Nullable AccountType input) {
+                return  input.areContactsWritable() &&
+                        GoogleAccountType.ACCOUNT_TYPE.equals(input.accountType);
+
+            }
+        }));
+    }
+
+    /**
+     * Returns true if there are real accounts (not "local" account) in the list of accounts.
+     */
+    public boolean hasNonLocalAccount() {
+        final List<AccountWithDataSet> allAccounts = getAccounts(/* contactWritableOnly */ false);
+        if (allAccounts == null || allAccounts.size() == 0) {
+            return false;
+        }
+        if (allAccounts.size() > 1) {
+            return true;
+        }
+        return !allAccounts.get(0).isNullAccount();
+    }
+
     static Account getDefaultGoogleAccount(AccountManager accountManager,
             SharedPreferences prefs, String defaultAccountKey) {
         // Get all the google accounts on the device
@@ -258,53 +303,33 @@
             }
         };
     }
-}
 
-class AccountComparator implements Comparator<AccountWithDataSet> {
-    private AccountWithDataSet mDefaultAccount;
-
-    public AccountComparator(AccountWithDataSet defaultAccount) {
-        mDefaultAccount = defaultAccount;
+    public static Predicate<AccountWithDataSet> adaptTypeFilter(
+            final Predicate<AccountType> typeFilter, final AccountTypeProvider provider) {
+        return new Predicate<AccountWithDataSet>() {
+            @Override
+            public boolean apply(@Nullable AccountWithDataSet input) {
+                return typeFilter.apply(provider.getTypeForAccount(input));
+            }
+        };
     }
 
-    @Override
-    public int compare(AccountWithDataSet a, AccountWithDataSet b) {
-        if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
-                && Objects.equal(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 if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
-            return -1;
-        } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
-            return 1;
-        } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
-            return -1;
-        } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
-            return 1;
-        } else {
-            int diff = a.name.compareToIgnoreCase(b.name);
-            if (diff != 0) {
-                return diff;
+    public static Predicate<AccountType> writableFilter() {
+        return new Predicate<AccountType>() {
+            @Override
+            public boolean apply(@Nullable AccountType account) {
+                return account.areContactsWritable();
             }
-            diff = a.type.compareToIgnoreCase(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.compareToIgnoreCase(b.dataSet);
-            } else {
-                return -1;
-            }
-        }
+        };
     }
 
-    private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
-        return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
+    public static Predicate<AccountType> groupWritableFilter() {
+        return new Predicate<AccountType>() {
+            @Override
+            public boolean apply(@Nullable AccountType account) {
+                return account.isGroupMembershipEditable();
+            }
+        };
     }
 }
 
@@ -313,68 +338,49 @@
 
     private Context mContext;
     private AccountManager mAccountManager;
-    private DeviceLocalAccountTypeFactory mDeviceLocalAccountTypeFactory;
+    private DeviceLocalAccountLocator mLocalAccountLocator;
+    private AccountTypeProvider mTypeProvider;
+    private ListeningExecutorService mExecutor;
+    private Executor mMainThreadExecutor;
 
     private AccountType mFallbackAccountType;
 
-    private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
-    private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
-    private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
-    private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
+    private ListenableFuture<List<AccountWithDataSet>> mLocalAccountsFuture;
+    private ListenableFuture<AccountTypeProvider> mAccountTypesFuture;
 
-    private static final int MESSAGE_LOAD_DATA = 0;
-    private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+    private FutureCallback<Object> mAccountsUpdateCallback = new FutureCallback<Object>() {
+        @Override
+        public void onSuccess(@Nullable Object result) {
+            onAccountsUpdatedInternal();
+        }
 
-    private HandlerThread mListenerThread;
-    private Handler mListenerHandler;
+        @Override
+        public void onFailure(Throwable t) {
+        }
+    };
 
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
-    private final Runnable mCheckFilterValidityRunnable = new Runnable () {
-        @Override
-        public void run() {
-            ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
-        }
-    };
 
     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);
+            reloadAccountTypes();
         }
-
     };
 
-    /* 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,
-            DeviceLocalAccountTypeFactory deviceLocalAccountTypeFactory) {
+    public AccountTypeManagerImpl(Context context) {
         mContext = context;
+        mLocalAccountLocator = DeviceLocalAccountLocator.create(context);
+        mTypeProvider = new AccountTypeProvider(context);
         mFallbackAccountType = new FallbackAccountType(context);
-        mDeviceLocalAccountTypeFactory = deviceLocalAccountTypeFactory;
 
         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;
-                }
-            }
-        };
+        mExecutor = ContactsExecutors.getDefaultThreadPoolExecutor();
+        mMainThreadExecutor = ContactsExecutors.newHandlerExecutor(mMainThreadHandler);
 
         // Request updates when packages or accounts change
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
@@ -392,17 +398,16 @@
         filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
         mContext.registerReceiver(mBroadcastReceiver, filter);
 
-        mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+        mAccountManager.addOnAccountsUpdatedListener(this, mMainThreadHandler, false);
 
         ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
 
-
         if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
             // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
             // if a new device contact is added.
             mContext.getContentResolver().registerContentObserver(
                     ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
-                    new ContentObserver(mListenerHandler) {
+                    new ContentObserver(mMainThreadHandler) {
                         @Override
                         public boolean deliverSelfNotifications() {
                             return true;
@@ -410,294 +415,73 @@
 
                         @Override
                         public void onChange(boolean selfChange) {
-                            mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+                            reloadLocalAccounts();
                         }
 
                         @Override
                         public void onChange(boolean selfChange, Uri uri) {
-                            mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+                            reloadLocalAccounts();
                         }
                     });
         }
-
-        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+        loadAccountTypes();
     }
 
     @Override
     public void onStatusChanged(int which) {
-        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
-    }
-
-    public void processBroadcastIntent(Intent intent) {
-        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+        reloadAccountTypes();
     }
 
     /* This notification will arrive on the background thread */
     public void onAccountsUpdated(Account[] accounts) {
-        // Refresh to catch any changed accounts
-        loadAccountsInBackground();
+        onAccountsUpdatedInternal();
     }
 
-    /**
-     * 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;
-        }
+    private void onAccountsUpdatedInternal() {
+        ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+        LocalBroadcastManager.getInstance(mContext).sendBroadcast(
+                new Intent(BROADCAST_ACCOUNTS_CHANGED));
+    }
 
-        while (true) {
-            try {
-                latch.await();
-                return;
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
+    private synchronized void startLoadingIfNeeded() {
+        if (mTypeProvider == null && mAccountTypesFuture == null) {
+            reloadAccountTypes();
+        }
+        if (mLocalAccountsFuture == null) {
+            reloadLocalAccounts();
         }
     }
 
-    /**
-     * 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();
+    private void loadAccountTypes() {
+        mTypeProvider = new AccountTypeProvider(mContext);
 
-        // Account types, keyed off the account type and data set concatenation.
-        final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet =
-                Maps.newHashMap();
-
-        // 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 = Maps.newHashMap();
-
-        final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
-        final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
-        final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
-        final Set<String> extensionPackages = Sets.newHashSet();
-
-        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;
+        mAccountTypesFuture = mExecutor.submit(new Callable<AccountTypeProvider>() {
+            @Override
+            public AccountTypeProvider call() throws Exception {
+                // This will request the AccountType for each Account
+                getAccountsFromProvider(mTypeProvider);
+                return mTypeProvider;
             }
-
-            // 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 if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
-                    && isLocalAccountType(mDeviceLocalAccountTypeFactory, type)) {
-                // This will be loaded by the DeviceLocalAccountLocator so don't try to create an
-                // ExternalAccountType for it.
-                continue;
-            } 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.initializeFieldsFromAuthenticator(auth);
-
-            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");
-
-        boolean foundWritableGoogleAccount = false;
-        // 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 || GoogleAccountType.ACCOUNT_TYPE.equals(account.type)) {
-                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 (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
-                                    && accountWithDataSet.dataSet == null) {
-                                foundWritableGoogleAccount = true;
-                            }
-
-                            if (accountType.isGroupMembershipEditable()) {
-                                groupWritableAccounts.add(accountWithDataSet);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        final DeviceLocalAccountLocator deviceAccountLocator = DeviceLocalAccountLocator
-                .create(mContext, allAccounts);
-        final List<AccountWithDataSet> localAccounts = deviceAccountLocator
-                .getDeviceLocalAccounts();
-        allAccounts.addAll(localAccounts);
-
-        for (AccountWithDataSet localAccount : localAccounts) {
-            // Prefer a known type if it exists. This covers the case that a local account has an
-            // authenticator with a valid contacts.xml
-            AccountType localAccountType = accountTypesByTypeAndDataSet.get(
-                    localAccount.getAccountTypeWithDataSet());
-            if (localAccountType == null) {
-                localAccountType = mDeviceLocalAccountTypeFactory.getAccountType(localAccount.type);
-            }
-            accountTypesByTypeAndDataSet.put(localAccount.getAccountTypeWithDataSet(),
-                    localAccountType);
-
-            // Skip the null account if there is a Google account available. This is done because
-            // the Google account's sync adapter will automatically move accounts in the "null"
-            // account.  Hence, it would be confusing to still show it as an available writable
-            // account since contacts that were saved to it would magically change accounts when the
-            // sync adapter runs.
-            if (foundWritableGoogleAccount && localAccount.type == null) {
-                continue;
-            }
-            if (localAccountType.areContactsWritable()) {
-                contactWritableAccounts.add(localAccount);
-
-                if (localAccountType.isGroupMembershipEditable()) {
-                    groupWritableAccounts.add(localAccount);
-                }
-            }
-        }
-
-        final AccountComparator accountComparator = new AccountComparator(null);
-        Collections.sort(allAccounts, accountComparator);
-        Collections.sort(contactWritableAccounts, accountComparator);
-        Collections.sort(groupWritableAccounts, accountComparator);
-
-        timings.addSplit("Loaded accounts");
-
-        synchronized (this) {
-            mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
-            mAccounts = allAccounts;
-            mContactWritableAccounts = contactWritableAccounts;
-            mGroupWritableAccounts = groupWritableAccounts;
-        }
-
-        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 = Lists.newArrayList();
-        }
-        accountsForType.add(accountType);
-        accountTypesByType.put(accountType.accountType, accountsForType);
+    private synchronized void reloadAccountTypes() {
+        loadAccountTypes();
+        Futures.addCallback(mAccountTypesFuture, mAccountsUpdateCallback, mMainThreadExecutor);
     }
 
-    /**
-     * 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;
+    private synchronized void loadLocalAccounts() {
+        mLocalAccountsFuture = mExecutor.submit(new Callable<List<AccountWithDataSet>>() {
+            @Override
+            public List<AccountWithDataSet> call() throws Exception {
+                return mLocalAccountLocator.getDeviceLocalAccounts();
             }
-        }
-        return null;
+        });
+    }
+
+    private void reloadLocalAccounts() {
+        loadLocalAccounts();
+        Futures.addCallback(mLocalAccountsFuture, mAccountsUpdateCallback, mMainThreadExecutor);
     }
 
     /**
@@ -706,21 +490,101 @@
      */
     @Override
     public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
-        ensureAccountsLoaded();
-        return Lists.newArrayList(contactWritableOnly ? mContactWritableAccounts : mAccounts);
+        final Predicate<AccountType> filter = contactWritableOnly ?
+                writableFilter() : Predicates.<AccountType>alwaysTrue();
+        // TODO: Shouldn't have a synchronous version for getting all accounts
+        return Futures.getUnchecked(filterAccountsByTypeAsync(filter));
     }
 
     @Override
     public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
-        return new ArrayList<>(Collections2.filter(mAccounts, filter));
+        // TODO: Shouldn't have a synchronous version for getting all accounts
+        return Futures.getUnchecked(filterAccountsAsync(filter));
     }
 
+    @Override
+    public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+        startLoadingIfNeeded();
+        return filterAccountsAsync(Predicates.<AccountWithDataSet>alwaysTrue());
+    }
+
+    @Override
+    public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+            final Predicate<AccountType> typeFilter) {
+        // Ensure that mTypeProvider is initialized so that the reference will be the same
+        // here as in the call to filterAccountsAsync
+        startLoadingIfNeeded();
+        return filterAccountsAsync(adaptTypeFilter(typeFilter, mTypeProvider));
+    }
+
+    private ListenableFuture<List<AccountWithDataSet>> filterAccountsAsync(
+            final Predicate<AccountWithDataSet> filter) {
+        startLoadingIfNeeded();
+        final ListenableFuture<List<AccountWithDataSet>> accountsFromTypes =
+                Futures.transform(Futures.nonCancellationPropagating(mAccountTypesFuture),
+                        new Function<AccountTypeProvider, List<AccountWithDataSet>>() {
+                            @Override
+                            public List<AccountWithDataSet> apply(AccountTypeProvider provider) {
+                                return getAccountsFromProvider(provider);
+                            }
+                        });
+
+        final ListenableFuture<List<List<AccountWithDataSet>>> all =
+                Futures.successfulAsList(accountsFromTypes, mLocalAccountsFuture);
+
+        return Futures.transform(all, new Function<List<List<AccountWithDataSet>>,
+                List<AccountWithDataSet>>() {
+            @Nullable
+            @Override
+            public List<AccountWithDataSet> apply(@Nullable List<List<AccountWithDataSet>> input) {
+                // The first result list is from the account types. Check if there is a Google
+                // account in this list and if there is exclude the null account
+                final Predicate<AccountWithDataSet> appliedFilter =
+                        hasWritableGoogleAccount(input.get(0)) ?
+                                Predicates.and(nonNullAccountFilter(), filter) :
+                                filter;
+                List<AccountWithDataSet> result = new ArrayList<>();
+                for (List<AccountWithDataSet> list : input) {
+                    if (list != null) {
+                        result.addAll(Collections2.filter(list, appliedFilter));
+                    }
+                }
+                return result;
+            }
+        });
+    }
+
+    private List<AccountWithDataSet> getAccountsFromProvider(AccountTypeProvider cache) {
+        final List<AccountWithDataSet> result = new ArrayList<>();
+        final Account[] accounts = mAccountManager.getAccounts();
+        for (Account account : accounts) {
+            final List<AccountType> types = cache.getAccountTypes(account.type);
+            for (AccountType type : types) {
+                result.add(new AccountWithDataSet(account.name, account.type, type.dataSet));
+            }
+        }
+        return result;
+    }
+
+    private boolean hasWritableGoogleAccount(List<AccountWithDataSet> accounts) {
+        if (accounts == null) {
+            return false;
+        }
+        AccountType type;
+        for (AccountWithDataSet account : accounts) {
+            if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet ==  null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
     /**
      * Return the list of all known, group writable {@link AccountWithDataSet}'s.
      */
     public List<AccountWithDataSet> getGroupWritableAccounts() {
-        ensureAccountsLoaded();
-        return Lists.newArrayList(mGroupWritableAccounts);
+        return Futures.getUnchecked(filterAccountsByTypeAsync(groupWritableFilter()));
     }
 
     /**
@@ -729,12 +593,29 @@
      */
     @Override
     public Account getDefaultGoogleAccount() {
-        final AccountManager accountManager = AccountManager.get(mContext);
         final SharedPreferences sharedPreferences =
                 mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE);
         final String defaultAccountKey =
                 mContext.getResources().getString(R.string.contact_editor_default_account_key);
-        return getDefaultGoogleAccount(accountManager, sharedPreferences, defaultAccountKey);
+        return getDefaultGoogleAccount(mAccountManager, sharedPreferences, defaultAccountKey);
+    }
+
+    @Override
+    public List<AccountWithDataSet> getWritableGoogleAccounts() {
+        final Account[] googleAccounts =
+                mAccountManager.getAccountsByType(GoogleAccountType.ACCOUNT_TYPE);
+        final List<AccountWithDataSet> result = new ArrayList<>();
+        for (Account account : googleAccounts) {
+            // Accounts with a dataSet (e.g. Google plus accounts) are not writable.
+            result.add(new AccountWithDataSet(account.name, account.type, null));
+        }
+        return result;
+    }
+
+    @Override
+    public boolean hasNonLocalAccount() {
+        final Account[] accounts = mAccountManager.getAccounts();
+        return accounts != null && accounts.length > 0;
     }
 
     /**
@@ -744,7 +625,6 @@
      */
     @Override
     public DataKind getKindOrFallback(AccountType type, String mimeType) {
-        ensureAccountsLoaded();
         DataKind kind = null;
 
         // Try finding account type and kind matching request
@@ -771,10 +651,7 @@
      */
     @Override
     public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
-        ensureAccountsLoaded();
-        synchronized (this) {
-            AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
-            return type != null ? type : mFallbackAccountType;
-        }
+        return mTypeProvider.getType(
+                accountTypeWithDataSet.accountType, accountTypeWithDataSet.dataSet);
     }
 }
diff --git a/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java b/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
index afd7917..307577d 100644
--- a/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/model/Cp2DeviceLocalAccountLocator.java
@@ -60,14 +60,10 @@
 
     public Cp2DeviceLocalAccountLocator(ContentResolver contentResolver,
             DeviceLocalAccountTypeFactory factory,
-            List<AccountWithDataSet> knownAccounts) {
+            Set<String> knownAccountTypes) {
         mResolver = contentResolver;
         mAccountTypeFactory = factory;
 
-        final Set<String> knownAccountTypes = new HashSet<>();
-        for (AccountWithDataSet account : knownAccounts) {
-            knownAccountTypes.add(account.type);
-        }
         mSelection = getSelection(knownAccountTypes);
         mSelectionArgs = getSelectionArgs(knownAccountTypes);
     }
diff --git a/src/com/android/contacts/model/DeviceLocalAccountLocator.java b/src/com/android/contacts/model/DeviceLocalAccountLocator.java
index 7fb9ef8..0d34057 100644
--- a/src/com/android/contacts/model/DeviceLocalAccountLocator.java
+++ b/src/com/android/contacts/model/DeviceLocalAccountLocator.java
@@ -15,6 +15,8 @@
  */
 package com.android.contacts.model;
 
+import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.content.Context;
 
 import com.android.contacts.Experiments;
@@ -23,7 +25,9 @@
 import com.android.contactsbind.experiments.Flags;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Attempts to detect accounts for device contacts
@@ -45,10 +49,24 @@
     };
 
     public static DeviceLocalAccountLocator create(Context context,
-            List<AccountWithDataSet> knownAccounts) {
+            Set<String> knownAccountTypes) {
         if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
             return new Cp2DeviceLocalAccountLocator(context.getContentResolver(),
-                    ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownAccounts);
+                    ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownAccountTypes);
+        }
+        return NULL_ONLY;
+    }
+
+    public static DeviceLocalAccountLocator create(Context context) {
+        final AccountManager accountManager =
+                (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+        final Set<String> knownTypes = new HashSet<>();
+        for (Account account : accountManager.getAccounts()) {
+            knownTypes.add(account.type);
+        }
+        if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
+            return new Cp2DeviceLocalAccountLocator(context.getContentResolver(),
+                    ObjectFactory.getDeviceLocalAccountTypeFactory(context), knownTypes);
         }
         return NULL_ONLY;
     }
diff --git a/src/com/android/contacts/model/account/AccountComparator.java b/src/com/android/contacts/model/account/AccountComparator.java
new file mode 100644
index 0000000..70ccf49
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountComparator.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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.model.account;
+
+import com.google.common.base.Objects;
+
+import java.util.Comparator;
+
+/**
+ * Orders accounts for display such that the default account is first
+ */
+public class AccountComparator implements Comparator<AccountWithDataSet> {
+    private AccountWithDataSet mDefaultAccount;
+
+    public AccountComparator(AccountWithDataSet defaultAccount) {
+        mDefaultAccount = defaultAccount;
+    }
+
+    @Override
+    public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+        if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
+                && Objects.equal(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 if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
+            return -1;
+        } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
+            return 1;
+        } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
+            return -1;
+        } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
+            return 1;
+        } else {
+            int diff = a.name.compareToIgnoreCase(b.name);
+            if (diff != 0) {
+                return diff;
+            }
+            diff = a.type.compareToIgnoreCase(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.compareToIgnoreCase(b.dataSet);
+            } else {
+                return -1;
+            }
+        }
+    }
+
+    private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
+        return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
+    }
+}
diff --git a/src/com/android/contacts/model/account/AccountTypeProvider.java b/src/com/android/contacts/model/account/AccountTypeProvider.java
new file mode 100644
index 0000000..474b3b4
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountTypeProvider.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2016 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.model.account;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.provider.ContactsContract;
+import android.support.v4.util.ArraySet;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.util.DeviceLocalAccountTypeFactory;
+import com.android.contactsbind.ObjectFactory;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static com.android.contacts.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
+
+/**
+ * Provides access to {@link AccountType}s with contact data
+ *
+ * This class parses the contacts.xml for third-party accounts and caches the result.
+ * This means that {@link AccountTypeProvider#getAccountTypes(String)}} should be called from a
+ * background thread.
+ */
+public class AccountTypeProvider {
+    private static final String TAG = "AccountTypeProvider";
+
+    private final Context mContext;
+    private final DeviceLocalAccountTypeFactory mLocalAccountTypeFactory;
+    private final ImmutableMap<String, AuthenticatorDescription> mAuthTypes;
+
+    private final ConcurrentMap<String, List<AccountType>> mCache = new ConcurrentHashMap<>();
+
+    public AccountTypeProvider(Context context) {
+        this(context,
+                ObjectFactory.getDeviceLocalAccountTypeFactory(context),
+                ContentResolver.getSyncAdapterTypes(),
+                ((AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE))
+                        .getAuthenticatorTypes());
+    }
+
+    public AccountTypeProvider(Context context, DeviceLocalAccountTypeFactory localTypeFactory,
+            SyncAdapterType[] syncAdapterTypes,
+            AuthenticatorDescription[] authenticatorDescriptions) {
+        mContext = context;
+        mLocalAccountTypeFactory = localTypeFactory;
+
+        final Set<String> mContactSyncableTypes = new ArraySet<>();
+        for (SyncAdapterType type : syncAdapterTypes) {
+            if (type.authority.equals(ContactsContract.AUTHORITY)) {
+                mContactSyncableTypes.add(type.accountType);
+            }
+        }
+
+        final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
+                ImmutableMap.builder();
+        for (AuthenticatorDescription auth : authenticatorDescriptions) {
+            if (mContactSyncableTypes.contains(auth.type)) {
+                builder.put(auth.type, auth);
+            }
+        }
+        mAuthTypes = builder.build();
+    }
+
+    /**
+     * Returns all account types associated with the provided type
+     *
+     * <p>There are many {@link AccountType}s for each accountType because {@AccountType} includes
+     * a dataSet and accounts can declare extension packages in contacts.xml that provide additional
+     * data sets for a particular type
+     * </p>
+     */
+    public List<AccountType> getAccountTypes(String accountType) {
+        // ConcurrentHashMap doesn't support null keys
+        if (accountType == null) {
+            AccountType type = mLocalAccountTypeFactory.getAccountType(accountType);
+            // Just in case the DeviceLocalAccountTypeFactory doesn't handle the null type
+            if (type == null) {
+                type = new FallbackAccountType(mContext);
+            }
+            return Collections.singletonList(type);
+        }
+
+        List<AccountType> types = mCache.get(accountType);
+        if (types == null) {
+            types = loadTypes(accountType);
+            mCache.put(accountType, types);
+        }
+        return types;
+    }
+
+    public boolean hasTypeForAccount(AccountWithDataSet account) {
+        return getTypeForAccount(account) != null;
+    }
+
+    public boolean hasTypeWithDataset(String type, String dataSet) {
+        // getAccountTypes() never returns null
+        final List<AccountType> accountTypes = getAccountTypes(type);
+        for (AccountType accountType : accountTypes) {
+            if (Objects.equal(accountType.dataSet, dataSet)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the AccountType with the matching type and dataSet or null if no account with those
+     * members exists
+     */
+    public AccountType getType(String type, String dataSet) {
+        final List<AccountType> accountTypes = getAccountTypes(type);
+        for (AccountType accountType : accountTypes) {
+            if (Objects.equal(accountType.dataSet, dataSet)) {
+                return accountType;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the AccountType for a particular account or null if no account type exists for the
+     * account
+     */
+    public AccountType getTypeForAccount(AccountWithDataSet account) {
+        return getType(account.type, account.dataSet);
+    }
+
+    private List<AccountType> loadTypes(String type) {
+        final AuthenticatorDescription auth = mAuthTypes.get(type);
+        if (auth == null) {
+            return Collections.emptyList();
+        }
+
+        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 if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
+                && isLocalAccountType(mLocalAccountTypeFactory, type)) {
+            accountType = mLocalAccountTypeFactory.getAccountType(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
+                return Collections.emptyList();
+            }
+        }
+
+        accountType.initializeFieldsFromAuthenticator(auth);
+
+        final ImmutableList.Builder<AccountType> result = ImmutableList.builder();
+        result.add(accountType);
+
+        for (String extensionPackage : accountType.getExtensionPackageNames()) {
+            final ExternalAccountType extensionType =
+                    new ExternalAccountType(mContext, extensionPackage, true);
+            if (!extensionType.isInitialized()) {
+                // Skip external account types that couldn't be initialized.
+                continue;
+            }
+            if (!extensionType.hasContactsMetadata()) {
+                Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+                        + " it doesn't have the CONTACTS_STRUCTURE metadata");
+                continue;
+            }
+            if (TextUtils.isEmpty(extensionType.accountType)) {
+                Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+                        + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+                        + " attribute");
+                continue;
+            }
+            if (Objects.equal(extensionType.accountType, type)) {
+                Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+                        + " the account type + " + extensionType.accountType +
+                        " doesn't match expected type " + type);
+                continue;
+            }
+            Log.d(TAG, "Registering extension package account type="
+                    + accountType.accountType + ", dataSet=" + accountType.dataSet
+                    + ", packageName=" + extensionPackage);
+
+            result.add(extensionType);
+        }
+        return result.build();
+    }
+
+}
diff --git a/src/com/android/contacts/util/AccountFilterUtil.java b/src/com/android/contacts/util/AccountFilterUtil.java
index 3a6d49e..9eb8e7b 100644
--- a/src/com/android/contacts/util/AccountFilterUtil.java
+++ b/src/com/android/contacts/util/AccountFilterUtil.java
@@ -21,11 +21,14 @@
 import android.app.Fragment;
 import android.content.ActivityNotFoundException;
 import android.content.AsyncTaskLoader;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Intents;
+import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
@@ -36,18 +39,27 @@
 import com.android.contacts.list.ContactListFilter;
 import com.android.contacts.list.ContactListFilterController;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.Contact;
 import com.android.contacts.model.account.AccountDisplayInfo;
 import com.android.contacts.model.account.AccountDisplayInfoFactory;
 import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferences;
+import com.android.contacts.util.concurrent.ContactsExecutors;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
 import com.android.contactsbind.ObjectFactory;
 
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * Utility class for account filter manipulation.
  */
@@ -98,65 +110,78 @@
     /**
      * Loads a list of contact list filters
      */
-    public static class FilterLoader extends AsyncTaskLoader<List<ContactListFilter>> {
-        private Context mContext;
+    public static class FilterLoader extends ListenableFutureLoader<List<ContactListFilter>> {
+        private AccountTypeManager mAccountTypeManager;
         private DeviceLocalAccountTypeFactory mDeviceLocalFactory;
+        private LocalBroadcastManager mLocalBroadcastManager;
+        private BroadcastReceiver mReceiver;
 
         public FilterLoader(Context context) {
             super(context);
-            mContext = context;
+            mAccountTypeManager = AccountTypeManager.getInstance(context);
             mDeviceLocalFactory = ObjectFactory.getDeviceLocalAccountTypeFactory(context);
-        }
-
-        @Override
-        public List<ContactListFilter> loadInBackground() {
-            return loadAccountFilters(mContext, mDeviceLocalFactory);
+            mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
         }
 
         @Override
         protected void onStartLoading() {
-            forceLoad();
-        }
-
-        @Override
-        protected void onStopLoading() {
-            cancelLoad();
+            super.onStartLoading();
+            if (mReceiver == null) {
+                mReceiver = new ForceLoadReceiver();
+                mLocalBroadcastManager.registerReceiver(mReceiver,
+                        new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+            }
         }
 
         @Override
         protected void onReset() {
-            onStopLoading();
-        }
-    }
-
-    private static List<ContactListFilter> loadAccountFilters(Context context,
-            DeviceLocalAccountTypeFactory deviceAccountTypeFactory) {
-        final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
-        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
-        final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(true);
-        AccountTypeManager.sortAccounts(getDefaultAccount(context), accounts);
-
-        for (AccountWithDataSet account : accounts) {
-            final AccountType accountType =
-                    accountTypeManager.getAccountType(account.type, account.dataSet);
-            if ((accountType.isExtension() || DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
-                    deviceAccountTypeFactory, account.type)) && !account.hasData(context)) {
-                // Hide extensions and device accounts with no raw_contacts.
-                continue;
-            }
-            final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
-            if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
-                    deviceAccountTypeFactory, account.type)) {
-                accountFilters.add(ContactListFilter.createDeviceContactsFilter(icon, account));
-            } else {
-                accountFilters.add(ContactListFilter.createAccountFilter(
-                        account.type, account.name, account.dataSet, icon));
+            super.onReset();
+            if (mReceiver != null) {
+                mLocalBroadcastManager.unregisterReceiver(mReceiver);
             }
         }
 
-        final ArrayList<ContactListFilter> result = Lists.newArrayList();
-        result.addAll(accountFilters);
-        return result;
+        @Override
+        protected ListenableFuture<List<ContactListFilter>> loadData() {
+            return Futures.transform(mAccountTypeManager.filterAccountsByTypeAsync(
+                    AccountTypeManager.writableFilter()),
+                    new Function<List<AccountWithDataSet>, List<ContactListFilter>>() {
+                        @Override
+                        public List<ContactListFilter> apply(List<AccountWithDataSet> input) {
+                            return getFiltersForAccounts(input);
+                        }
+                    }, ContactsExecutors.getDefaultThreadPoolExecutor());
+        }
+
+        private List<ContactListFilter> getFiltersForAccounts(List<AccountWithDataSet> accounts) {
+            final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
+            AccountTypeManager.sortAccounts(getDefaultAccount(getContext()), accounts);
+
+            for (AccountWithDataSet account : accounts) {
+                final AccountType accountType =
+                        mAccountTypeManager.getAccountType(account.type, account.dataSet);
+                if ((accountType.isExtension() ||
+                        DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
+                                mDeviceLocalFactory, account.type)) &&
+                        !account.hasData(getContext())) {
+                    // Hide extensions and device accounts with no raw_contacts.
+                    continue;
+                }
+                final Drawable icon = accountType != null ?
+                        accountType.getDisplayIcon(getContext()) : null;
+                if (DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
+                        mDeviceLocalFactory, account.type)) {
+                    accountFilters.add(ContactListFilter.createDeviceContactsFilter(icon, account));
+                } else {
+                    accountFilters.add(ContactListFilter.createAccountFilter(
+                            account.type, account.name, account.dataSet, icon));
+                }
+            }
+
+            final ArrayList<ContactListFilter> result = Lists.newArrayList();
+            result.addAll(accountFilters);
+            return result;
+        }
     }
 
     private static AccountWithDataSet getDefaultAccount(Context context) {
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index 256123e..94a7c29 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -25,9 +25,11 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
+import com.android.contacts.list.ContactListFilter;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.account.AccountDisplayInfo;
 import com.android.contacts.model.account.AccountDisplayInfoFactory;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountWithDataSet;
 
 import java.util.ArrayList;
@@ -39,30 +41,53 @@
 public final class AccountsListAdapter extends BaseAdapter {
     private final LayoutInflater mInflater;
     private final List<AccountDisplayInfo> mAccountDisplayInfoList;
+    private final List<AccountWithDataSet> mAccounts;
     private final Context mContext;
     private int mCustomLayout = -1;
 
-    /**
-     * Filters that affect the list of accounts that is displayed by this adapter.
-     */
     public enum AccountListFilter {
-        ALL_ACCOUNTS,                   // All read-only and writable accounts
-        ACCOUNTS_CONTACT_WRITABLE,      // Only where the account type is contact writable
-        ACCOUNTS_GROUP_WRITABLE         // Only accounts where the account type is group writable
+        ALL_ACCOUNTS {
+            @Override
+            public List<AccountWithDataSet> getAccounts(Context context) {
+                return AccountTypeManager.getInstance(context).getAccounts(false);
+            }
+        },
+        ACCOUNTS_CONTACT_WRITABLE {
+            @Override
+            public List<AccountWithDataSet> getAccounts(Context context) {
+                return AccountTypeManager.getInstance(context).getAccounts(true);
+            }
+        },
+        ACCOUNTS_GROUP_WRITABLE {
+            @Override
+            public List<AccountWithDataSet> getAccounts(Context context) {
+                return AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+            }
+        };
+
+        public abstract List<AccountWithDataSet> getAccounts(Context context);
     }
 
-    public AccountsListAdapter(Context context, AccountListFilter accountListFilter) {
-        this(context, accountListFilter, null);
+    public AccountsListAdapter(Context context, AccountListFilter filter) {
+        this(context, filter.getAccounts(context), null);
+    }
+
+    public AccountsListAdapter(Context context, AccountListFilter filter,
+            AccountWithDataSet currentAccount) {
+        this(context, filter.getAccounts(context), currentAccount);
+    }
+
+    public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts) {
+        this(context, accounts, null);
     }
 
     /**
      * @param currentAccount the Account currently selected by the user, which should come
      * first in the list. Can be null.
      */
-    public AccountsListAdapter(Context context, AccountListFilter accountListFilter,
+    public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts,
             AccountWithDataSet currentAccount) {
         mContext = context;
-        final List<AccountWithDataSet> accounts = getAccounts(accountListFilter);
         if (currentAccount != null
                 && !accounts.isEmpty()
                 && !accounts.get(0).equals(currentAccount)
@@ -77,15 +102,8 @@
             mAccountDisplayInfoList.add(factory.getAccountDisplayInfo(account));
         }
         mInflater = LayoutInflater.from(context);
-    }
 
-    private List<AccountWithDataSet> getAccounts(AccountListFilter accountListFilter) {
-        final AccountTypeManager typeManager = AccountTypeManager.getInstance(mContext);
-        if (accountListFilter == AccountListFilter.ACCOUNTS_GROUP_WRITABLE) {
-            return new ArrayList<AccountWithDataSet>(typeManager.getGroupWritableAccounts());
-        }
-        return new ArrayList<AccountWithDataSet>(typeManager.getAccounts(
-                accountListFilter == AccountListFilter.ACCOUNTS_CONTACT_WRITABLE));
+        mAccounts = accounts;
     }
 
     public void setCustomLayout(int customLayout) {
diff --git a/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java b/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
index 3e61555..59ee7e5 100644
--- a/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
+++ b/src/com/android/contacts/util/DeviceLocalAccountTypeFactory.java
@@ -15,8 +15,6 @@
  */
 package com.android.contacts.util;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
 import android.content.Context;
 import android.support.annotation.IntDef;
 
@@ -25,6 +23,8 @@
 
 import java.lang.annotation.Retention;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 /**
  * Reports whether a value from RawContacts.ACCOUNT_TYPE should be considered a "Device"
  * account
diff --git a/src/com/android/contacts/util/SyncUtil.java b/src/com/android/contacts/util/SyncUtil.java
index 0d6c494..ce10937 100644
--- a/src/com/android/contacts/util/SyncUtil.java
+++ b/src/com/android/contacts/util/SyncUtil.java
@@ -22,8 +22,11 @@
 import android.net.NetworkInfo;
 import android.provider.ContactsContract;
 
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.model.account.GoogleAccountType;
 
+import java.util.List;
+
 /**
  * Utilities related to sync.
  */
@@ -46,6 +49,19 @@
     }
 
     /**
+     * Returns true {@link ContentResolver#isSyncPending(Account, String)} or
+     * {@link ContentResolver#isSyncActive(Account, String)} is true for any account in accounts
+     */
+    public static final boolean isAnySyncing(List<AccountWithDataSet> accounts) {
+        for (AccountWithDataSet accountWithDataSet : accounts) {
+            if (isSyncStatusPendingOrActive(accountWithDataSet.getAccountOrNull())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Returns true if the given Google account is not syncable.
      */
     public static final boolean isUnsyncableGoogleAccount(Account account) {
diff --git a/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
new file mode 100644
index 0000000..f7edb64
--- /dev/null
+++ b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 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.util.concurrent;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+
+/**
+ * Wraps a ListenableFuture for integration with {@link android.app.LoaderManager}
+ *
+ * <p>Using a loader ensures that the result is delivered while the receiving component (activity
+ * or fragment) is resumed and also prevents leaking references these components
+ * </p>
+ */
+public abstract class ListenableFutureLoader<D> extends Loader<D> {
+    private static final String TAG = "FutureLoader";
+
+    private ListenableFuture<D> mFuture;
+    private D mLoadedData;
+    private Executor mUiExecutor;
+
+    /**
+     * Stores away the application context associated with context.
+     * Since Loaders can be used across multiple activities it's dangerous to
+     * store the context directly; always use {@link #getContext()} to retrieve
+     * the Loader's Context, don't use the constructor argument directly.
+     * The Context returned by {@link #getContext} is safe to use across
+     * Activity instances.
+     *
+     * @param context used to retrieve the application context.
+     */
+    public ListenableFutureLoader(Context context) {
+        super(context);
+        mUiExecutor = ContactsExecutors.newUiThreadExecutor();
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mLoadedData != null) {
+            deliverResult(mLoadedData);
+        }
+        if (mFuture == null) {
+            takeContentChanged();
+            forceLoad();
+        } else if (takeContentChanged()) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onForceLoad() {
+        mFuture = loadData();
+        Futures.addCallback(mFuture, new FutureCallback<D>() {
+            @Override
+            public void onSuccess(D result) {
+                mLoadedData = result;
+                deliverResult(mLoadedData);
+                commitContentChanged();
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                if (t instanceof CancellationException) {
+                    Log.i(TAG, "Loading cancelled", t);
+                    rollbackContentChanged();
+                } else {
+                    Log.e(TAG, "Failed to load accounts", t);
+                }
+            }
+        }, mUiExecutor);
+    }
+
+    @Override
+    protected void onStopLoading() {
+        if (mFuture != null) {
+            mFuture.cancel(false);
+            mFuture = null;
+        }
+    }
+
+    @Override
+    protected void onReset() {
+        mFuture = null;
+        mLoadedData = null;
+    }
+
+    protected abstract ListenableFuture<D> loadData();
+
+    public class ForceLoadReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            onContentChanged();
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java b/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
index e6e67bf..4e62126 100644
--- a/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
+++ b/tests/src/com/android/contacts/model/Cp2DeviceLocalAccountLocatorTests.java
@@ -24,6 +24,7 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.RawContacts;
 import android.support.annotation.Nullable;
+import android.support.v4.util.ArraySet;
 import android.test.AndroidTestCase;
 import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -48,7 +49,7 @@
         final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
                 getContext().getContentResolver(),
                 new DeviceLocalAccountTypeFactory.Default(getContext()),
-                Collections.<AccountWithDataSet>emptyList());
+                Collections.<String>emptySet());
         sut.getDeviceLocalAccounts();
         // We didn't throw so it passed
     }
@@ -80,31 +81,29 @@
         final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
                 .withDeviceTypes(null, "vnd.sec.contact.phone")
                 .withSimTypes("vnd.sec.contact.sim");
-        final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
-                createStubResolverWithContentQueryResult(queryResult(
-                        "user", "com.example",
-                        "user", "com.example",
-                        "phone_account", "vnd.sec.contact.phone",
-                        null, null,
-                        "phone_account", "vnd.sec.contact.phone",
-                        "user", "com.example",
-                        null, null,
-                        "sim_account", "vnd.sec.contact.sim",
-                        "sim_account_2", "vnd.sec.contact.sim"
-                )), stubFactory,
-                Collections.<AccountWithDataSet>emptyList());
+        final DeviceLocalAccountLocator sut = createLocator(queryResult(
+                "user", "com.example",
+                "user", "com.example",
+                "phone_account", "vnd.sec.contact.phone",
+                null, null,
+                "phone_account", "vnd.sec.contact.phone",
+                "user", "com.example",
+                null, null,
+                "sim_account", "vnd.sec.contact.sim",
+                "sim_account_2", "vnd.sec.contact.sim"
+        ), stubFactory);
+
 
         assertEquals(4, sut.getDeviceLocalAccounts().size());
     }
 
-    public void test_getDeviceLocalAccounts_doesNotContainItemsForKnownAccounts() {
+    public void test_getDeviceLocalAccounts_doesNotContainItemsForKnownAccountTypes() {
         final Cp2DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
                 getContext().getContentResolver(), new FakeDeviceAccountTypeFactory(),
-                Arrays.asList(new AccountWithDataSet("user", "com.example", null),
-                        new AccountWithDataSet("user1", "com.example", null),
-                        new AccountWithDataSet("user", "com.example.1", null)));
+                new ArraySet<>(Arrays.asList("com.example", "com.example.1")));
 
-        assertTrue("Selection should filter known accounts", sut.getSelection().contains("NOT IN (?,?)"));
+        assertTrue("Selection should filter known accounts",
+                sut.getSelection().contains("NOT IN (?,?)"));
 
         final List<String> args = Arrays.asList(sut.getSelectionArgs());
         assertEquals(2, args.size());
@@ -116,12 +115,11 @@
         final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
                 .withDeviceTypes(null, "vnd.sec.contact.phone")
                 .withSimTypes("vnd.sec.contact.sim");
-        final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
-                createContentResolverWithProvider(new FakeContactsProvider()
-                        .withQueryResult(ContactsContract.Settings.CONTENT_URI, queryResult(
-                                "phone_account", "vnd.sec.contact.phone",
-                                "sim_account", "vnd.sec.contact.sim"
-                ))), stubFactory, Collections.<AccountWithDataSet>emptyList());
+        final DeviceLocalAccountLocator sut = createLocator(new FakeContactsProvider()
+                .withQueryResult(ContactsContract.Settings.CONTENT_URI, queryResult(
+                        "phone_account", "vnd.sec.contact.phone",
+                        "sim_account", "vnd.sec.contact.sim"
+                )), stubFactory);
 
         assertEquals(2, sut.getDeviceLocalAccounts().size());
     }
@@ -130,22 +128,34 @@
         final DeviceLocalAccountTypeFactory stubFactory = new FakeDeviceAccountTypeFactory()
                 .withDeviceTypes(null, "vnd.sec.contact.phone")
                 .withSimTypes("vnd.sec.contact.sim");
-        final DeviceLocalAccountLocator sut = new Cp2DeviceLocalAccountLocator(
-                createContentResolverWithProvider(new FakeContactsProvider()
-                        .withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
-                                "phone_account", "vnd.sec.contact.phone",
-                                "sim_account", "vnd.sec.contact.sim"
-                        ))), stubFactory, Collections.<AccountWithDataSet>emptyList());
+        final DeviceLocalAccountLocator sut = createLocator(new FakeContactsProvider()
+                .withQueryResult(ContactsContract.Groups.CONTENT_URI, queryResult(
+                        "phone_account", "vnd.sec.contact.phone",
+                        "sim_account", "vnd.sec.contact.sim"
+                )), stubFactory);
 
         assertEquals(2, sut.getDeviceLocalAccounts().size());
     }
 
     private DeviceLocalAccountLocator createWithQueryResult(
             Cursor cursor) {
+        return createLocator(cursor, new DeviceLocalAccountTypeFactory.Default(mContext));
+    }
+
+    private DeviceLocalAccountLocator createLocator(ContentProvider contactsProvider,
+            DeviceLocalAccountTypeFactory localAccountTypeFactory) {
+        final DeviceLocalAccountLocator locator = new Cp2DeviceLocalAccountLocator(
+                createContentResolverWithProvider(contactsProvider),
+                localAccountTypeFactory, Collections.<String>emptySet());
+        return locator;
+    }
+
+    private DeviceLocalAccountLocator createLocator(Cursor cursor,
+            DeviceLocalAccountTypeFactory localAccountTypeFactory) {
         final DeviceLocalAccountLocator locator = new Cp2DeviceLocalAccountLocator(
                 createStubResolverWithContentQueryResult(cursor),
-                new DeviceLocalAccountTypeFactory.Default(getContext()),
-                Collections.<AccountWithDataSet>emptyList());
+                localAccountTypeFactory,
+                Collections.<String>emptySet());
         return locator;
     }
 
@@ -155,7 +165,6 @@
         return resolver;
     }
 
-
     private ContentResolver createStubResolverWithContentQueryResult(Cursor cursor) {
         final MockContentResolver resolver = new MockContentResolver();
         resolver.addProvider(ContactsContract.AUTHORITY, new FakeContactsProvider()
diff --git a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
index a69c877..e1c370a 100644
--- a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
@@ -22,16 +22,15 @@
 import com.android.contacts.model.account.AccountTypeWithDataSet;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.model.account.BaseAccountType;
-
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 /**
  * A mock {@link AccountTypeManager} class.
@@ -76,6 +75,17 @@
     }
 
     @Override
+    public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+        return Futures.immediateFuture(Arrays.asList(mAccounts));
+    }
+
+    @Override
+    public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
+            Predicate<AccountType> type) {
+        return Futures.immediateFuture(Arrays.asList(mAccounts));
+    }
+
+    @Override
     public List<AccountWithDataSet> getGroupWritableAccounts() {
         return Arrays.asList(mAccounts);
     }