Merge changes I5e0bf85f,I3c92c80f,I39962f99 into ub-contactsdialer-i-dev

* changes:
  Only broadcast account change if accounts are different
  Use loader for accounts in editor account chooser
  Add AccountInfo class
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index 8d8e2db..a8db4ee 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -47,6 +47,7 @@
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.SimCard;
 import com.android.contacts.model.SimContact;
+import com.android.contacts.model.account.AccountInfo;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferences;
 import com.android.contacts.util.concurrent.ContactsExecutors;
@@ -258,15 +259,15 @@
     public void onLoaderReset(Loader<LoaderResult> loader) {
     }
 
-    private void restoreAdapterSelectedStates(List<AccountWithDataSet> accounts) {
+    private void restoreAdapterSelectedStates(List<AccountInfo> accounts) {
         if (mSavedInstanceState == null) {
             return;
         }
 
-        for (AccountWithDataSet account : accounts) {
+        for (AccountInfo account : accounts) {
             final long[] selections = mSavedInstanceState.getLongArray(
-                    account.stringify() + KEY_SUFFIX_SELECTED_IDS);
-            mPerAccountCheckedIds.put(account, selections);
+                    account.getAccount().stringify() + KEY_SUFFIX_SELECTED_IDS);
+            mPerAccountCheckedIds.put(account.getAccount(), selections);
         }
         mSavedInstanceState = null;
     }
@@ -446,10 +447,8 @@
         private AccountTypeManager mAccountTypeManager;
         private final int mSubscriptionId;
 
-        private BroadcastReceiver mReceiver;
-
         public SimContactLoader(Context context, int subscriptionId) {
-            super(context);
+            super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
             mDao = SimContactDao.create(context);
             mAccountTypeManager = AccountTypeManager.getInstance(getContext());
             mSubscriptionId = subscriptionId;
@@ -459,7 +458,7 @@
         protected ListenableFuture<LoaderResult> loadData() {
             final ListenableFuture<List<Object>> future = Futures.<Object>allAsList(
                     mAccountTypeManager
-                            .filterAccountsByTypeAsync(AccountTypeManager.writableFilter()),
+                            .filterAccountsAsync(AccountTypeManager.writableFilter()),
                     ContactsExecutors.getSimReadExecutor().<Object>submit(
                             new Callable<Object>() {
                         @Override
@@ -470,7 +469,7 @@
             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 List<AccountInfo> accounts = (List<AccountInfo>) input.get(0);
                     final LoaderResult simLoadResult = (LoaderResult) input.get(1);
                     simLoadResult.accounts = accounts;
                     return simLoadResult;
@@ -490,26 +489,10 @@
             result.accountsMap = mDao.findAccountsOfExistingSimContacts(result.contacts);
             return result;
         }
-
-        @Override
-        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 List<AccountInfo> accounts;
         public ArrayList<SimContact> contacts;
         public Map<AccountWithDataSet, Set<SimContact>> accountsMap;
     }
diff --git a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
index 50e11f3..3531125 100644
--- a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
@@ -18,8 +18,10 @@
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.LoaderManager;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.Loader;
 import android.os.Bundle;
 import android.provider.ContactsContract.Intents;
 import android.view.View;
@@ -33,10 +35,13 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.ContactEditorUtils;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountInfo;
 import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.account.AccountsLoader;
 import com.android.contacts.util.AccountsListAdapter;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.util.ImplicitIntentsUtil;
+import com.google.common.util.concurrent.Futures;
 
 import java.util.List;
 
@@ -48,7 +53,8 @@
  * the new contact in. If the activity result doesn't contain intent data, then there is no
  * account for this contact.
  */
-public class ContactEditorAccountsChangedActivity extends Activity {
+public class ContactEditorAccountsChangedActivity extends Activity
+        implements LoaderManager.LoaderCallbacks<List<AccountInfo>> {
 
     private static final String TAG = ContactEditorAccountsChangedActivity.class.getSimpleName();
 
@@ -95,10 +101,30 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
         mEditorUtils = ContactEditorUtils.create(this);
-        final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(this).
-                getAccounts(true);
+        getLoaderManager().initLoader(0, null, this);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == SUBACTIVITY_ADD_NEW_ACCOUNT) {
+            // If the user canceled the account setup process, then keep this activity visible to
+            // the user.
+            if (resultCode != RESULT_OK) {
+                return;
+            }
+            // Subactivity was successful, so pass the result back and finish the activity.
+            AccountWithDataSet account = mEditorUtils.getCreatedAccount(resultCode, data);
+            if (account == null) {
+                setResult(resultCode);
+                finish();
+                return;
+            }
+            saveAccountAndReturnResult(account);
+        }
+    }
+
+    private void updateDisplayedAccounts(List<AccountInfo> accounts) {
         final int numAccounts = accounts.size();
         if (numAccounts < 0) {
             throw new IllegalStateException("Cannot have a negative number of accounts");
@@ -123,7 +149,7 @@
                     AccountListFilter.ACCOUNTS_CONTACT_WRITABLE);
             accountListView.setAdapter(mAccountListAdapter);
             accountListView.setOnItemClickListener(mAccountListItemClickListener);
-        } else if (numAccounts == 1 && !accounts.get(0).isNullAccount()) {
+        } else if (numAccounts == 1 && !accounts.get(0).getAccount().isNullAccount()) {
             // If the user has 1 writable account we will just show the user a message with 2
             // possible action buttons.
             view = View.inflate(this,
@@ -133,9 +159,9 @@
             final Button leftButton = (Button) view.findViewById(R.id.left_button);
             final Button rightButton = (Button) view.findViewById(R.id.right_button);
 
-            final AccountWithDataSet account = accounts.get(0);
+            final AccountInfo accountInfo = accounts.get(0);
             textView.setText(getString(R.string.contact_editor_prompt_one_account,
-                    account.name));
+                    accountInfo.getNameLabel()));
 
             // This button allows the user to add a new account to the device and return to
             // this app afterwards.
@@ -148,7 +174,7 @@
             rightButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
-                    saveAccountAndReturnResult(account);
+                    saveAccountAndReturnResult(accountInfo.getAccount());
                 }
             });
         } else {
@@ -183,6 +209,9 @@
             rightButton.setOnClickListener(mAddAccountClickListener);
         }
 
+        if (mDialog != null && mDialog.isShowing()) {
+            mDialog.dismiss();
+        }
         mDialog = new AlertDialog.Builder(this)
                 .setView(view)
                 .setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -195,25 +224,6 @@
         mDialog.show();
     }
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == SUBACTIVITY_ADD_NEW_ACCOUNT) {
-            // If the user canceled the account setup process, then keep this activity visible to
-            // the user.
-            if (resultCode != RESULT_OK) {
-                return;
-            }
-            // Subactivity was successful, so pass the result back and finish the activity.
-            AccountWithDataSet account = mEditorUtils.getCreatedAccount(resultCode, data);
-            if (account == null) {
-                setResult(resultCode);
-                finish();
-                return;
-            }
-            saveAccountAndReturnResult(account);
-        }
-    }
-
     private void saveAccountAndReturnResult(AccountWithDataSet account) {
         // Save this as the default account
         mEditorUtils.saveDefaultAccount(account);
@@ -224,4 +234,18 @@
         setResult(RESULT_OK, intent);
         finish();
     }
+
+    @Override
+    public Loader<List<AccountInfo>> onCreateLoader(int id, Bundle args) {
+        return new AccountsLoader(this, AccountTypeManager.writableFilter());
+    }
+
+    @Override
+    public void onLoadFinished(Loader<List<AccountInfo>> loader, List<AccountInfo> data) {
+        updateDisplayedAccounts(data);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<List<AccountInfo>> loader) {
+    }
 }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 426dcd7..28891e5 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.AccountInfo;
 import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountFilterUtil;
@@ -172,7 +173,8 @@
                 accounts = Collections.singletonList(new AccountWithDataSet(filter.accountName,
                         filter.accountType, null));
             } else if (filter.shouldShowSyncState()) {
-                accounts = mAccountTypeManager.getWritableGoogleAccounts();
+                accounts = AccountInfo.extractAccounts(
+                        mAccountTypeManager.getWritableGoogleAccounts());
             } else {
                 accounts = Collections.emptyList();
             }
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
index 0185419..e7e8e50 100644
--- a/src/com/android/contacts/editor/AccountHeaderPresenter.java
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -26,9 +26,7 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
-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.AccountInfo;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountsListAdapter;
 import com.android.contacts.util.UiClosables;
@@ -56,9 +54,8 @@
     }
 
     private final Context mContext;
-    private AccountDisplayInfoFactory mAccountDisplayInfoFactory;
 
-    private List<AccountWithDataSet> mAccounts;
+    private List<AccountInfo> mAccounts;
     private AccountWithDataSet mCurrentAccount;
 
     // Account header
@@ -100,19 +97,18 @@
         updateDisplayedAccount();
     }
 
-    public void setAccounts(List<AccountWithDataSet> accounts) {
+    public void setAccounts(List<AccountInfo> 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);
+        if (mCurrentAccount != null && !AccountInfo.contains(mAccounts, mCurrentAccount)) {
+            mCurrentAccount = mAccounts.isEmpty() ? null : accounts.get(0).getAccount();
             mObserver.onChange(this);
         }
         updateDisplayedAccount();
     }
 
     public AccountWithDataSet getCurrentAccount() {
-        return mCurrentAccount;
+        return mCurrentAccount != null ? mCurrentAccount : null;
     }
 
     public void onSaveInstanceState(Bundle outState) {
@@ -132,10 +128,7 @@
         if (mCurrentAccount == null) return;
         if (mAccounts == null) return;
 
-        final AccountDisplayInfo account =
-                mAccountDisplayInfoFactory.getAccountDisplayInfo(mCurrentAccount);
-
-        final String accountLabel = getAccountLabel(account);
+        final String accountLabel = getAccountLabel(mCurrentAccount);
 
         if (mAccounts.size() > 1) {
             addAccountSelector(accountLabel);
@@ -157,10 +150,10 @@
             mAccountHeaderType.setText(selectorTitle);
         }
 
+        final AccountInfo accountInfo = AccountInfo.getAccount(mAccounts, mCurrentAccount);
+
         // Set the icon
-        final AccountDisplayInfo displayInfo = mAccountDisplayInfoFactory
-                .getAccountDisplayInfo(mCurrentAccount);
-        mAccountHeaderIcon.setImageDrawable(displayInfo.getIcon());
+        mAccountHeaderIcon.setImageDrawable(accountInfo.getIcon());
 
         // Set the content description
         mAccountHeaderContainer.setContentDescription(
@@ -217,8 +210,8 @@
         mAccountHeaderContainer.setOnClickListener(listener);
     }
 
-    private String getAccountLabel(AccountDisplayInfo account) {
-        // TODO: if used from editor this would need to be different if editing the user's profile.
-        return account.getNameLabel().toString();
+    private String getAccountLabel(AccountWithDataSet account) {
+        final AccountInfo accountInfo = AccountInfo.getAccount(mAccounts, account);
+        return accountInfo != null ? accountInfo.getNameLabel().toString() : null;
     }
 }
diff --git a/src/com/android/contacts/list/CustomContactListFilterActivity.java b/src/com/android/contacts/list/CustomContactListFilterActivity.java
index 3ca8e36..bbde17b 100644
--- a/src/com/android/contacts/list/CustomContactListFilterActivity.java
+++ b/src/com/android/contacts/list/CustomContactListFilterActivity.java
@@ -23,13 +23,13 @@
 import android.app.DialogFragment;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.app.ProgressDialog;
-import android.content.AsyncTaskLoader;
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.Loader;
 import android.content.OperationApplicationException;
 import android.database.Cursor;
@@ -59,16 +59,18 @@
 import com.android.contacts.R;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.ValuesDelta;
-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.AccountInfo;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.model.account.GoogleAccountType;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.LocalizedNameResolver;
 import com.android.contacts.util.WeakAsyncTask;
-
+import com.android.contacts.util.concurrent.ContactsExecutors;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
+import com.google.common.base.Function;
 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.Collections;
@@ -76,6 +78,8 @@
 import java.util.Iterator;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * Shows a list of all available {@link Groups} available, letting the user
  * select which ones they want to be visible.
@@ -128,38 +132,38 @@
         }
     }
 
-    public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> {
+    public static class CustomFilterConfigurationLoader extends ListenableFutureLoader<AccountSet> {
 
-        private AccountSet mAccountSet;
+        private AccountTypeManager mAccountTypeManager;
 
         public CustomFilterConfigurationLoader(Context context) {
-            super(context);
+            super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+            mAccountTypeManager = AccountTypeManager.getInstance(context);
         }
 
         @Override
-        public AccountSet loadInBackground() {
-            Context context = getContext();
-            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
+        public ListenableFuture<AccountSet> loadData() {
+            return Futures.transform(mAccountTypeManager.getAccountsAsync(),
+                    new Function<List<AccountInfo>, AccountSet>() {
+                @Nullable
+                @Override
+                public AccountSet apply(@Nullable List<AccountInfo> input) {
+                    return createAccountSet(input);
+                }
+            }, ContactsExecutors.getDefaultThreadPoolExecutor());
+        }
+
+        private AccountSet createAccountSet(List<AccountInfo> sourceAccounts) {
+            final Context context = getContext();
             final ContentResolver resolver = context.getContentResolver();
 
             final AccountSet accounts = new AccountSet();
 
             // Don't include the null account because it doesn't support writing to
             // ContactsContract.Settings
-            final List<AccountWithDataSet> sourceAccounts = accountTypes.getAccounts(
-                    AccountTypeManager.nonNullAccountFilter());
-            final AccountDisplayInfoFactory displayableAccountFactory =
-                    new AccountDisplayInfoFactory(context, sourceAccounts);
-            for (AccountWithDataSet account : sourceAccounts) {
-                final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
-                if (accountType.isExtension() && !account.hasData(context)) {
-                    // Extension with no data -- skip.
-                    continue;
-                }
-
-                AccountDisplay accountDisplay =
-                        new AccountDisplay(resolver, account.name, account.type, account.dataSet,
-                                displayableAccountFactory.getAccountDisplayInfo(account));
+            for (AccountInfo info : sourceAccounts) {
+                final AccountWithDataSet account = info.getAccount();
+                final AccountDisplay accountDisplay = new AccountDisplay(resolver, info);
 
                 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
                         .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
@@ -197,41 +201,6 @@
 
             return accounts;
         }
-
-        @Override
-        public void deliverResult(AccountSet cursor) {
-            if (isReset()) {
-                return;
-            }
-
-            mAccountSet = cursor;
-
-            if (isStarted()) {
-                super.deliverResult(cursor);
-            }
-        }
-
-        @Override
-        protected void onStartLoading() {
-            if (mAccountSet != null) {
-                deliverResult(mAccountSet);
-            }
-            if (takeContentChanged() || mAccountSet == null) {
-                forceLoad();
-            }
-        }
-
-        @Override
-        protected void onStopLoading() {
-            cancelLoad();
-        }
-
-        @Override
-        protected void onReset() {
-            super.onReset();
-            onStopLoading();
-            mAccountSet = null;
-        }
     }
 
     @Override
@@ -479,7 +448,7 @@
         public final String mName;
         public final String mType;
         public final String mDataSet;
-        public final AccountDisplayInfo mAccountDisplayInfo;
+        public final AccountInfo mAccountInfo;
 
         public GroupDelta mUngrouped;
         public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
@@ -497,12 +466,11 @@
          * Build an {@link AccountDisplay} covering all {@link Groups} under the
          * given {@link AccountWithDataSet}.
          */
-        public AccountDisplay(ContentResolver resolver, String accountName, String accountType,
-                String dataSet, AccountDisplayInfo displayableInfo) {
-            mName = accountName;
-            mType = accountType;
-            mDataSet = dataSet;
-            mAccountDisplayInfo = displayableInfo;
+        public AccountDisplay(ContentResolver resolver, AccountInfo accountInfo) {
+            mName = accountInfo.getAccount().name;
+            mType = accountInfo.getAccount().type;
+            mDataSet = accountInfo.getAccount().dataSet;
+            mAccountInfo = accountInfo;
         }
 
         /**
@@ -616,11 +584,11 @@
 
             final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
 
-            text1.setText(account.mAccountDisplayInfo.getNameLabel());
-            text1.setVisibility(!account.mAccountDisplayInfo.isDeviceAccount()
-                    || account.mAccountDisplayInfo.hasDistinctName()
+            text1.setText(account.mAccountInfo.getNameLabel());
+            text1.setVisibility(!account.mAccountInfo.isDeviceAccount()
+                    || account.mAccountInfo.hasDistinctName()
                     ? View.VISIBLE : View.GONE);
-            text2.setText(account.mAccountDisplayInfo.getTypeLabel());
+            text2.setText(account.mAccountInfo.getTypeLabel());
 
             final int textColor = mContext.getResources().getColor(isExpanded
                     ? R.color.dialtacts_theme_color
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index d87b444..fa8d6e2 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -40,7 +40,7 @@
 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.AccountInfo;
 import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountTypeProvider;
 import com.android.contacts.model.account.AccountTypeWithDataSet;
@@ -50,10 +50,13 @@
 import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.concurrent.ContactsExecutors;
 import com.android.contactsbind.experiments.Flags;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Function;
+import com.google.common.base.Objects;
 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.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -120,19 +123,19 @@
         }
 
         @Override
-        public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
-            return Collections.emptyList();
+        public ListenableFuture<List<AccountInfo>> getAccountsAsync() {
+            return Futures.immediateFuture(Collections.<AccountInfo>emptyList());
         }
 
         @Override
-        public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
-            return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+        public ListenableFuture<List<AccountInfo>> filterAccountsAsync(
+                Predicate<AccountInfo> filter) {
+            return Futures.immediateFuture(Collections.<AccountInfo>emptyList());
         }
 
         @Override
-        public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
-                Predicate<AccountType> type) {
-            return Futures.immediateFuture(Collections.<AccountWithDataSet>emptyList());
+        public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) {
+            return null;
         }
 
         @Override
@@ -158,15 +161,18 @@
     // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
     public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
 
-    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<AccountInfo>> getAccountsAsync();
 
-    public abstract ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
-            Predicate<AccountType> type);
+    /**
+     * Loads accounts and applies the fitler returning only for which the predicate is true
+     */
+    public abstract ListenableFuture<List<AccountInfo>> filterAccountsAsync(
+            Predicate<AccountInfo> filter);
+
+    public abstract AccountInfo getAccountInfoForAccount(AccountWithDataSet account);
 
     /**
      * Returns the list of accounts that are group writable.
@@ -185,14 +191,13 @@
      * to call synchronously.
      * </p>
      */
-    public List<AccountWithDataSet> getWritableGoogleAccounts() {
+    public List<AccountInfo> getWritableGoogleAccounts() {
         // This implementation may block and should be overridden by the Impl class
-        return Futures.getUnchecked(filterAccountsByTypeAsync(new Predicate<AccountType>() {
+        return Futures.getUnchecked(filterAccountsAsync(new Predicate<AccountInfo>() {
             @Override
-            public boolean apply(@Nullable AccountType input) {
-                return  input.areContactsWritable() &&
-                        GoogleAccountType.ACCOUNT_TYPE.equals(input.accountType);
-
+            public boolean apply(@Nullable AccountInfo input) {
+                return  input.getType().areContactsWritable() &&
+                        GoogleAccountType.ACCOUNT_TYPE.equals(input.getType().accountType);
             }
         }));
     }
@@ -278,15 +283,6 @@
         return getDefaultGoogleAccount() != null;
     }
 
-    /**
-     * Sorts the accounts in-place such that defaultAccount is first in the list and the rest
-     * of the accounts are ordered in manner that is useful for display purposes
-     */
-    public static void sortAccounts(AccountWithDataSet defaultAccount,
-            List<AccountWithDataSet> accounts) {
-        Collections.sort(accounts, new AccountComparator(defaultAccount));
-    }
-
     private static boolean hasRequiredPermissions(Context context) {
         final boolean canGetAccounts = ContextCompat.checkSelfPermission(context,
                 android.Manifest.permission.GET_ACCOUNTS) == PackageManager.PERMISSION_GRANTED;
@@ -295,39 +291,41 @@
         return canGetAccounts && canReadContacts;
     }
 
-    public static Predicate<AccountWithDataSet> nonNullAccountFilter() {
-        return new Predicate<AccountWithDataSet>() {
+    public static Predicate<AccountInfo> nonNullAccountFilter() {
+        return new Predicate<AccountInfo>() {
             @Override
-            public boolean apply(@Nullable AccountWithDataSet account) {
-                return account != null && account.name != null && account.type != null;
+            public boolean apply(AccountInfo info) {
+                AccountWithDataSet account = info != null ? info.getAccount() : null;
+                return account != null && !account.isNullAccount();
+            }
+        };
+
+    }
+
+    public static Predicate<AccountInfo> writableFilter() {
+        return new Predicate<AccountInfo>() {
+            @Override
+            public boolean apply(AccountInfo account) {
+                return account.getType().areContactsWritable();
             }
         };
     }
 
-    public static Predicate<AccountWithDataSet> adaptTypeFilter(
-            final Predicate<AccountType> typeFilter, final AccountTypeProvider provider) {
-        return new Predicate<AccountWithDataSet>() {
+    public static Predicate<AccountInfo> groupWritableFilter() {
+        return new Predicate<AccountInfo>() {
             @Override
-            public boolean apply(@Nullable AccountWithDataSet input) {
-                return typeFilter.apply(provider.getTypeForAccount(input));
+            public boolean apply(@Nullable AccountInfo account) {
+                return account.getType().isGroupMembershipEditable();
             }
         };
     }
 
-    public static Predicate<AccountType> writableFilter() {
-        return new Predicate<AccountType>() {
+    public static Predicate<AccountInfo> onlyNonEmptyExtensionFilter(Context context) {
+        final Context appContext = context.getApplicationContext();
+        return new Predicate<AccountInfo>() {
             @Override
-            public boolean apply(@Nullable AccountType account) {
-                return account.areContactsWritable();
-            }
-        };
-    }
-
-    public static Predicate<AccountType> groupWritableFilter() {
-        return new Predicate<AccountType>() {
-            @Override
-            public boolean apply(@Nullable AccountType account) {
-                return account.isGroupMembershipEditable();
+            public boolean apply(@Nullable AccountInfo input) {
+                return !input.getType().isExtension() || input.getAccount().hasData(appContext);
             }
         };
     }
@@ -336,34 +334,38 @@
 class AccountTypeManagerImpl extends AccountTypeManager
         implements OnAccountsUpdateListener, SyncStatusObserver {
 
-    private Context mContext;
-    private AccountManager mAccountManager;
-    private DeviceLocalAccountLocator mLocalAccountLocator;
+    private final Context mContext;
+    private final AccountManager mAccountManager;
+    private final DeviceLocalAccountLocator mLocalAccountLocator;
+    private final Executor mMainThreadExecutor;
+    private final ListeningExecutorService mExecutor;
     private AccountTypeProvider mTypeProvider;
-    private ListeningExecutorService mExecutor;
-    private Executor mMainThreadExecutor;
 
-    private AccountType mFallbackAccountType;
+    private final AccountType mFallbackAccountType;
 
     private ListenableFuture<List<AccountWithDataSet>> mLocalAccountsFuture;
     private ListenableFuture<AccountTypeProvider> mAccountTypesFuture;
 
-    private FutureCallback<Object> mAccountsUpdateCallback = new FutureCallback<Object>() {
-        @Override
-        public void onSuccess(@Nullable Object result) {
-            onAccountsUpdatedInternal();
-        }
-
-        @Override
-        public void onFailure(Throwable t) {
-        }
-    };
+    private List<AccountWithDataSet> mLocalAccounts = new ArrayList<>();
+    private List<AccountWithDataSet> mAccountManagerAccounts = new ArrayList<>();
 
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
+    private final Function<AccountTypeProvider, List<AccountWithDataSet>> mAccountsExtractor =
+            new Function<AccountTypeProvider, List<AccountWithDataSet>>() {
+                @Nullable
+                @Override
+                public List<AccountWithDataSet> apply(@Nullable AccountTypeProvider typeProvider) {
+                    return getAccountsWithDataSets(mAccountManager.getAccounts(), typeProvider);
+                }
+            };
+
+
     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            // Don't use reloadAccountTypesIfNeeded when packages change in case a contacts.xml
+            // was updated.
             reloadAccountTypes();
         }
     };
@@ -429,15 +431,26 @@
 
     @Override
     public void onStatusChanged(int which) {
-        reloadAccountTypes();
+        reloadAccountTypesIfNeeded();
     }
 
-    /* This notification will arrive on the background thread */
+    /* This notification will arrive on the UI thread */
     public void onAccountsUpdated(Account[] accounts) {
-        onAccountsUpdatedInternal();
+        maybeNotifyAccountsUpdated(mAccountManagerAccounts,
+                getAccountsWithDataSets(accounts, mTypeProvider));
     }
 
-    private void onAccountsUpdatedInternal() {
+    private void maybeNotifyAccountsUpdated(List<AccountWithDataSet> current,
+            List<AccountWithDataSet> update) {
+        if (Objects.equal(current, update)) {
+            return;
+        }
+        current.clear();
+        current.addAll(update);
+        notifyAccountsChanged();
+    }
+
+    private void notifyAccountsChanged() {
         ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
         LocalBroadcastManager.getInstance(mContext).sendBroadcast(
                 new Intent(BROADCAST_ACCOUNTS_CHANGED));
@@ -445,29 +458,53 @@
 
     private synchronized void startLoadingIfNeeded() {
         if (mTypeProvider == null && mAccountTypesFuture == null) {
-            reloadAccountTypes();
+            reloadAccountTypesIfNeeded();
         }
         if (mLocalAccountsFuture == null) {
             reloadLocalAccounts();
         }
     }
 
-    private void loadAccountTypes() {
+    private synchronized void loadAccountTypes() {
         mTypeProvider = new AccountTypeProvider(mContext);
 
         mAccountTypesFuture = mExecutor.submit(new Callable<AccountTypeProvider>() {
             @Override
             public AccountTypeProvider call() throws Exception {
-                // This will request the AccountType for each Account
-                getAccountsFromProvider(mTypeProvider);
+                // This will request the AccountType for each Account forcing them to be loaded
+                getAccountsWithDataSets(mAccountManager.getAccounts(), mTypeProvider);
                 return mTypeProvider;
             }
         });
     }
 
+    private FutureCallback<List<AccountWithDataSet>> newAccountsUpdatedCallback(
+            final List<AccountWithDataSet> currentAccounts) {
+        return new FutureCallback<List<AccountWithDataSet>>() {
+            @Override
+            public void onSuccess(List<AccountWithDataSet> result) {
+                maybeNotifyAccountsUpdated(currentAccounts, result);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+            }
+        };
+    }
+
+    private synchronized void reloadAccountTypesIfNeeded() {
+        if (mTypeProvider == null || mTypeProvider.shouldUpdate(
+                mAccountManager.getAuthenticatorTypes(), ContentResolver.getSyncAdapterTypes())) {
+            reloadAccountTypes();
+        }
+    }
+
     private synchronized void reloadAccountTypes() {
         loadAccountTypes();
-        Futures.addCallback(mAccountTypesFuture, mAccountsUpdateCallback, mMainThreadExecutor);
+        Futures.addCallback(
+                Futures.transform(mAccountTypesFuture, mAccountsExtractor),
+                newAccountsUpdatedCallback(mAccountManagerAccounts),
+                mMainThreadExecutor);
     }
 
     private synchronized void loadLocalAccounts() {
@@ -479,9 +516,10 @@
         });
     }
 
-    private void reloadLocalAccounts() {
+    private synchronized void reloadLocalAccounts() {
         loadLocalAccounts();
-        Futures.addCallback(mLocalAccountsFuture, mAccountsUpdateCallback, mMainThreadExecutor);
+        Futures.addCallback(mLocalAccountsFuture, newAccountsUpdatedCallback(mLocalAccounts),
+                mMainThreadExecutor);
     }
 
     /**
@@ -490,101 +528,103 @@
      */
     @Override
     public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
-        final Predicate<AccountType> filter = contactWritableOnly ?
-                writableFilter() : Predicates.<AccountType>alwaysTrue();
+        final Predicate<AccountInfo> filter = contactWritableOnly ?
+                writableFilter() : Predicates.<AccountInfo>alwaysTrue();
         // TODO: Shouldn't have a synchronous version for getting all accounts
-        return Futures.getUnchecked(filterAccountsByTypeAsync(filter));
+        return Lists.transform(Futures.getUnchecked(filterAccountsAsync(filter)),
+                AccountInfo.ACCOUNT_EXTRACTOR);
     }
 
     @Override
-    public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
-        // TODO: Shouldn't have a synchronous version for getting all accounts
-        return Futures.getUnchecked(filterAccountsAsync(filter));
+    public ListenableFuture<List<AccountInfo>> getAccountsAsync() {
+        return getAllAccountsAsyncInternal();
     }
 
-    @Override
-    public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
+    private synchronized ListenableFuture<List<AccountInfo>> getAllAccountsAsyncInternal() {
         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 AccountTypeProvider typeProvider = mTypeProvider;
         final ListenableFuture<List<List<AccountWithDataSet>>> all =
-                Futures.successfulAsList(accountsFromTypes, mLocalAccountsFuture);
+                Futures.nonCancellationPropagating(
+                        Futures.successfulAsList(
+                                Futures.transform(mAccountTypesFuture, mAccountsExtractor),
+                                mLocalAccountsFuture));
 
         return Futures.transform(all, new Function<List<List<AccountWithDataSet>>,
-                List<AccountWithDataSet>>() {
+                List<AccountInfo>>() {
             @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));
-                    }
+            public List<AccountInfo> apply(@Nullable List<List<AccountWithDataSet>> input) {
+                // input.get(0) contains accounts from AccountManager
+                // input.get(1) contains device local accounts
+                Preconditions.checkArgument(input.size() == 2,
+                        "List should have exactly 2 elements");
+
+                final List<AccountInfo> result = new ArrayList<>();
+                boolean hasWritableGoogleAccount = false;
+                for (AccountWithDataSet account : input.get(0)) {
+                    hasWritableGoogleAccount = hasWritableGoogleAccount ||
+                            (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) &&
+                                    account.dataSet == null);
+
+                    result.add(
+                            typeProvider.getTypeForAccount(account).wrapAccount(mContext, account));
                 }
+
+                for (AccountWithDataSet account : input.get(1)) {
+                    // Exclude the null account if a writable Google account exists because null
+                    // account contacts are automatically converted to Google contacts in this case
+                    if (hasWritableGoogleAccount && account.isNullAccount()) {
+                        continue;
+                    }
+                    result.add(
+                            typeProvider.getTypeForAccount(account).wrapAccount(mContext, account));
+                }
+                AccountInfo.sortAccounts(null, result);
                 return result;
             }
         });
     }
 
-    private List<AccountWithDataSet> getAccountsFromProvider(AccountTypeProvider cache) {
-        final List<AccountWithDataSet> result = new ArrayList<>();
-        final Account[] accounts = mAccountManager.getAccounts();
+    @Override
+    public ListenableFuture<List<AccountInfo>> filterAccountsAsync(
+            final Predicate<AccountInfo> filter) {
+        return Futures.transform(getAllAccountsAsyncInternal(), new Function<List<AccountInfo>,
+                List<AccountInfo>>() {
+            @Override
+            public List<AccountInfo> apply(List<AccountInfo> input) {
+                return new ArrayList<>(Collections2.filter(input, filter));
+            }
+        }, mExecutor);
+    }
+
+    @Override
+    public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) {
+        final AccountType type = mTypeProvider.getTypeForAccount(account);
+        if (type == null) {
+            return null;
+        }
+        return type.wrapAccount(mContext, account);
+    }
+
+    private List<AccountWithDataSet> getAccountsWithDataSets(Account[] accounts,
+            AccountTypeProvider typeProvider) {
+        List<AccountWithDataSet> result = new ArrayList<>();
         for (Account account : accounts) {
-            final List<AccountType> types = cache.getAccountTypes(account.type);
+            final List<AccountType> types = typeProvider.getAccountTypes(account.type);
             for (AccountType type : types) {
-                result.add(new AccountWithDataSet(account.name, account.type, type.dataSet));
+                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() {
-        return Futures.getUnchecked(filterAccountsByTypeAsync(groupWritableFilter()));
+        return Lists.transform(Futures.getUnchecked(
+                filterAccountsAsync(groupWritableFilter())), AccountInfo.ACCOUNT_EXTRACTOR);
     }
 
     /**
@@ -601,13 +641,17 @@
     }
 
     @Override
-    public List<AccountWithDataSet> getWritableGoogleAccounts() {
+    public List<AccountInfo> getWritableGoogleAccounts() {
         final Account[] googleAccounts =
                 mAccountManager.getAccountsByType(GoogleAccountType.ACCOUNT_TYPE);
-        final List<AccountWithDataSet> result = new ArrayList<>();
+        final List<AccountInfo> result = new ArrayList<>();
         for (Account account : googleAccounts) {
+            final AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
+                    account.name, account.type, null);
+            final AccountType type = mTypeProvider.getTypeForAccount(accountWithDataSet);
+
             // Accounts with a dataSet (e.g. Google plus accounts) are not writable.
-            result.add(new AccountWithDataSet(account.name, account.type, null));
+            result.add(type.wrapAccount(mContext, accountWithDataSet));
         }
         return result;
     }
diff --git a/src/com/android/contacts/model/account/AccountInfo.java b/src/com/android/contacts/model/account/AccountInfo.java
new file mode 100644
index 0000000..2161edb
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountInfo.java
@@ -0,0 +1,152 @@
+/*
+ * 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.graphics.drawable.Drawable;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Holds an {@link AccountWithDataSet} and the corresponding {@link AccountType} for an account.
+ */
+public class AccountInfo {
+
+    private final AccountDisplayInfo mDisplayInfo;
+    private final AccountType mType;
+
+    public AccountInfo(AccountDisplayInfo displayInfo, AccountType type) {
+        this.mDisplayInfo = displayInfo;
+        this.mType = type;
+    }
+
+    public AccountType getType() {
+        return mType;
+    }
+
+    public AccountWithDataSet getAccount() {
+        return mDisplayInfo.getSource();
+    }
+
+    /**
+     * Returns the displayable account name label for the account
+     */
+    public CharSequence getNameLabel() {
+        return mDisplayInfo.getNameLabel();
+    }
+
+    /**
+     * Returns the displayable account type label for the account
+     */
+    public CharSequence getTypeLabel() {
+        return mDisplayInfo.getTypeLabel();
+    }
+
+    /**
+     * Returns the icon for the account type
+     */
+    public Drawable getIcon() {
+        return mDisplayInfo.getIcon();
+    }
+
+    public boolean hasDistinctName() {
+        return mDisplayInfo.hasDistinctName();
+    }
+
+    public boolean isDeviceAccount() {
+        return mDisplayInfo.isDeviceAccount();
+    }
+
+    public boolean sameAccount(AccountInfo other) {
+        return sameAccount(other.getAccount());
+    }
+
+    public boolean sameAccount(AccountWithDataSet other) {
+        return Objects.equals(getAccount(), other);
+    }
+
+    /**
+     * Returns whether accounts contains an account that is the same as account
+     *
+     * <p>This does not use equality rather checks whether the source account ({@link #getAccount()}
+     * is the same</p>
+     */
+    public static boolean contains(List<AccountInfo> accounts, AccountInfo account) {
+        return contains(accounts, account.getAccount());
+    }
+
+    /**
+     * Returns whether accounts contains an account that is the same as account
+     *
+     * <p>This does not use equality rather checks whether the source account ({@link #getAccount()}
+     * is the same</p>
+     */
+    public static boolean contains(List<AccountInfo> accounts, AccountWithDataSet account) {
+        return getAccount(accounts, account) != null;
+    }
+
+    /**
+     * Returns the AccountInfo from the list that has the specified account as it's source account
+     */
+    public static AccountInfo getAccount(List<AccountInfo> accounts, AccountWithDataSet account) {
+        Preconditions.checkNotNull(accounts);
+
+        for (AccountInfo info : accounts) {
+            if (info.sameAccount(account)) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Sorts the accounts using the same ordering as {@link AccountComparator}
+     */
+    public static void sortAccounts(AccountWithDataSet defaultAccount, List<AccountInfo> accounts) {
+        Collections.sort(accounts, sourceComparator(defaultAccount));
+    }
+
+    /**
+     * Gets a list of the AccountWithDataSet for accounts
+     */
+    public static List<AccountWithDataSet> extractAccounts(List<AccountInfo> accounts) {
+        return Lists.transform(accounts, ACCOUNT_EXTRACTOR);
+    }
+
+    private static Comparator<AccountInfo> sourceComparator(AccountWithDataSet defaultAccount) {
+        final AccountComparator accountComparator = new AccountComparator(defaultAccount);
+        return new Comparator<AccountInfo>() {
+            @Override
+            public int compare(AccountInfo o1, AccountInfo o2) {
+                return accountComparator.compare(o1.getAccount(), o2.getAccount());
+            }
+        };
+    }
+
+    public static final Function<AccountInfo, AccountWithDataSet> ACCOUNT_EXTRACTOR =
+            new Function<AccountInfo, AccountWithDataSet>() {
+                @Override
+                public AccountWithDataSet apply(AccountInfo from) {
+                    return from.getAccount();
+                }
+            };
+}
diff --git a/src/com/android/contacts/model/account/AccountType.java b/src/com/android/contacts/model/account/AccountType.java
index f3462a1..bddfc09 100644
--- a/src/com/android/contacts/model/account/AccountType.java
+++ b/src/com/android/contacts/model/account/AccountType.java
@@ -31,7 +31,9 @@
 import com.android.contacts.R;
 import com.android.contacts.model.dataitem.DataKind;
 
+import com.google.common.base.Preconditions;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -183,6 +185,21 @@
     }
 
     /**
+     * Creates an {@link AccountInfo} for the specified account with the same type
+     *
+     * <p>The {@link AccountWithDataSet#type} must match {@link #accountType} of this instance</p>
+     */
+    public AccountInfo wrapAccount(Context context, AccountWithDataSet account) {
+        Preconditions.checkArgument(Objects.equal(account.type, accountType),
+                "Account types must match: account.type=%s but accountType=%s",
+                account.type, accountType);
+
+        return new AccountInfo(
+                new AccountDisplayInfo(account, account.name,
+                        getDisplayLabel(context), getDisplayIcon(context), false), this);
+    }
+
+    /**
      * @return resource ID for the "invite contact" action label, or -1 if not defined.
      */
     protected int getInviteContactActionResId() {
diff --git a/src/com/android/contacts/model/account/AccountTypeProvider.java b/src/com/android/contacts/model/account/AccountTypeProvider.java
index 474b3b4..5e64a7d 100644
--- a/src/com/android/contacts/model/account/AccountTypeProvider.java
+++ b/src/com/android/contacts/model/account/AccountTypeProvider.java
@@ -33,6 +33,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -69,21 +70,7 @@
         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();
+        mAuthTypes = onlyContactSyncable(authenticatorDescriptions, syncAdapterTypes);
     }
 
     /**
@@ -150,6 +137,19 @@
         return getType(account.type, account.dataSet);
     }
 
+    public boolean shouldUpdate(AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
+        Map<String, AuthenticatorDescription> contactsAuths = onlyContactSyncable(auths, syncTypes);
+        if (!contactsAuths.keySet().equals(mAuthTypes.keySet())) {
+            return false;
+        }
+        for (AuthenticatorDescription auth : contactsAuths.values()) {
+            if (!deepEquals(mAuthTypes.get(auth.type), auth)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private List<AccountType> loadTypes(String type) {
         final AuthenticatorDescription auth = mAuthTypes.get(type);
         if (auth == null) {
@@ -220,4 +220,38 @@
         return result.build();
     }
 
+    private static ImmutableMap<String, AuthenticatorDescription> onlyContactSyncable(
+            AuthenticatorDescription[] auths, SyncAdapterType[] syncTypes) {
+        final Set<String> mContactSyncableTypes = new ArraySet<>();
+        for (SyncAdapterType type : syncTypes) {
+            if (type.authority.equals(ContactsContract.AUTHORITY)) {
+                mContactSyncableTypes.add(type.accountType);
+            }
+        }
+
+        final ImmutableMap.Builder<String, AuthenticatorDescription> builder =
+                ImmutableMap.builder();
+        for (AuthenticatorDescription auth : auths) {
+            if (mContactSyncableTypes.contains(auth.type)) {
+                builder.put(auth.type, auth);
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Compares all fields in auth1 and auth2
+     *
+     * <p>By default {@link AuthenticatorDescription#equals(Object)} only checks the type</p>
+     */
+    private boolean deepEquals(AuthenticatorDescription auth1, AuthenticatorDescription auth2) {
+        return Objects.equal(auth1, auth2) &&
+                Objects.equal(auth1.packageName, auth2.packageName) &&
+                auth1.labelId == auth2.labelId &&
+                auth1.iconId == auth2.iconId &&
+                auth1.smallIconId == auth2.smallIconId &&
+                auth1.accountPreferencesId == auth2.accountPreferencesId &&
+                auth1.customTokens == auth2.customTokens;
+    }
+
 }
diff --git a/src/com/android/contacts/model/account/AccountsLoader.java b/src/com/android/contacts/model/account/AccountsLoader.java
new file mode 100644
index 0000000..78f309b
--- /dev/null
+++ b/src/com/android/contacts/model/account/AccountsLoader.java
@@ -0,0 +1,58 @@
+/*
+ * 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.content.Context;
+import android.content.IntentFilter;
+
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.util.concurrent.ListenableFutureLoader;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/**
+ * Loads the accounts from AccountTypeManager
+ */
+public class AccountsLoader extends ListenableFutureLoader<List<AccountInfo>> {
+    private final AccountTypeManager mAccountTypeManager;
+    private final Predicate<AccountInfo> mFilter;
+
+    public AccountsLoader(Context context) {
+        this(context, Predicates.<AccountInfo>alwaysTrue());
+    }
+
+    public AccountsLoader(Context context, Predicate<AccountInfo> filter) {
+        super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
+        mAccountTypeManager = AccountTypeManager.getInstance(context);
+        mFilter = filter;
+    }
+
+    @Override
+    protected ListenableFuture<List<AccountInfo>> loadData() {
+        return mAccountTypeManager.filterAccountsAsync(mFilter);
+    }
+
+    @Override
+    protected boolean isSameData(List<AccountInfo> previous, List<AccountInfo> next) {
+        return Objects.equal(AccountInfo.extractAccounts(previous),
+                AccountInfo.extractAccounts(next));
+    }
+
+}
diff --git a/src/com/android/contacts/model/account/DeviceLocalAccountType.java b/src/com/android/contacts/model/account/DeviceLocalAccountType.java
index 3941cf7..c6c7d07 100644
--- a/src/com/android/contacts/model/account/DeviceLocalAccountType.java
+++ b/src/com/android/contacts/model/account/DeviceLocalAccountType.java
@@ -34,4 +34,13 @@
     public boolean isGroupMembershipEditable() {
         return mGroupsEditable;
     }
+
+    @Override
+    public AccountInfo wrapAccount(Context context, AccountWithDataSet account) {
+        // Use the "Device" type label for the name as well because on OEM phones the "name" is
+        // not always user-friendly
+        return new AccountInfo(
+                new AccountDisplayInfo(account, getDisplayLabel(context), getDisplayLabel(context),
+                        getDisplayIcon(context), true), this);
+    }
 }
diff --git a/src/com/android/contacts/model/account/SimAccountType.java b/src/com/android/contacts/model/account/SimAccountType.java
index 636ee88..360e944 100644
--- a/src/com/android/contacts/model/account/SimAccountType.java
+++ b/src/com/android/contacts/model/account/SimAccountType.java
@@ -112,4 +112,13 @@
 
         return kind;
     }
+
+    @Override
+    public AccountInfo wrapAccount(Context context, AccountWithDataSet account) {
+        // Use the "SIM" type label for the name as well because on OEM phones the "name" is
+        // not always user-friendly
+        return new AccountInfo(
+                new AccountDisplayInfo(account, getDisplayLabel(context), getDisplayLabel(context),
+                        getDisplayIcon(context), true), this);
+    }
 }
diff --git a/src/com/android/contacts/util/AccountFilterUtil.java b/src/com/android/contacts/util/AccountFilterUtil.java
index 9eb8e7b..218604c 100644
--- a/src/com/android/contacts/util/AccountFilterUtil.java
+++ b/src/com/android/contacts/util/AccountFilterUtil.java
@@ -42,6 +42,7 @@
 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.AccountInfo;
 import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferences;
@@ -113,53 +114,33 @@
     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);
+            super(context, new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
             mAccountTypeManager = AccountTypeManager.getInstance(context);
             mDeviceLocalFactory = ObjectFactory.getDeviceLocalAccountTypeFactory(context);
-            mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
         }
 
-        @Override
-        protected void onStartLoading() {
-            super.onStartLoading();
-            if (mReceiver == null) {
-                mReceiver = new ForceLoadReceiver();
-                mLocalBroadcastManager.registerReceiver(mReceiver,
-                        new IntentFilter(AccountTypeManager.BROADCAST_ACCOUNTS_CHANGED));
-            }
-        }
-
-        @Override
-        protected void onReset() {
-            super.onReset();
-            if (mReceiver != null) {
-                mLocalBroadcastManager.unregisterReceiver(mReceiver);
-            }
-        }
 
         @Override
         protected ListenableFuture<List<ContactListFilter>> loadData() {
-            return Futures.transform(mAccountTypeManager.filterAccountsByTypeAsync(
+            return Futures.transform(mAccountTypeManager.filterAccountsAsync(
                     AccountTypeManager.writableFilter()),
-                    new Function<List<AccountWithDataSet>, List<ContactListFilter>>() {
+                    new Function<List<AccountInfo>, List<ContactListFilter>>() {
                         @Override
-                        public List<ContactListFilter> apply(List<AccountWithDataSet> input) {
+                        public List<ContactListFilter> apply(List<AccountInfo> input) {
                             return getFiltersForAccounts(input);
                         }
                     }, ContactsExecutors.getDefaultThreadPoolExecutor());
         }
 
-        private List<ContactListFilter> getFiltersForAccounts(List<AccountWithDataSet> accounts) {
-            final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
-            AccountTypeManager.sortAccounts(getDefaultAccount(getContext()), accounts);
+        private List<ContactListFilter> getFiltersForAccounts(List<AccountInfo> accounts) {
+            final ArrayList<ContactListFilter> accountFilters = new ArrayList<>();
+            AccountInfo.sortAccounts(getDefaultAccount(getContext()), accounts);
 
-            for (AccountWithDataSet account : accounts) {
-                final AccountType accountType =
-                        mAccountTypeManager.getAccountType(account.type, account.dataSet);
+            for (AccountInfo accountInfo : accounts) {
+                final AccountType accountType = accountInfo.getType();
+                final AccountWithDataSet account = accountInfo.getAccount();
                 if ((accountType.isExtension() ||
                         DeviceLocalAccountTypeFactory.Util.isLocalAccountType(
                                 mDeviceLocalFactory, account.type)) &&
@@ -178,9 +159,7 @@
                 }
             }
 
-            final ArrayList<ContactListFilter> result = Lists.newArrayList();
-            result.addAll(accountFilters);
-            return result;
+            return accountFilters;
         }
     }
 
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index 94a7c29..005bb8d 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -25,11 +25,8 @@
 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.AccountInfo;
 import com.android.contacts.model.account.AccountWithDataSet;
 
 import java.util.ArrayList;
@@ -40,32 +37,41 @@
  */
 public final class AccountsListAdapter extends BaseAdapter {
     private final LayoutInflater mInflater;
-    private final List<AccountDisplayInfo> mAccountDisplayInfoList;
-    private final List<AccountWithDataSet> mAccounts;
+    private final List<AccountInfo> mAccounts;
     private final Context mContext;
     private int mCustomLayout = -1;
 
     public enum AccountListFilter {
         ALL_ACCOUNTS {
             @Override
-            public List<AccountWithDataSet> getAccounts(Context context) {
+            public List<AccountWithDataSet> getSourceAccounts(Context context) {
                 return AccountTypeManager.getInstance(context).getAccounts(false);
             }
         },
         ACCOUNTS_CONTACT_WRITABLE {
             @Override
-            public List<AccountWithDataSet> getAccounts(Context context) {
+            public List<AccountWithDataSet> getSourceAccounts(Context context) {
                 return AccountTypeManager.getInstance(context).getAccounts(true);
             }
         },
         ACCOUNTS_GROUP_WRITABLE {
             @Override
-            public List<AccountWithDataSet> getAccounts(Context context) {
+            public List<AccountWithDataSet> getSourceAccounts(Context context) {
                 return AccountTypeManager.getInstance(context).getGroupWritableAccounts();
             }
         };
 
-        public abstract List<AccountWithDataSet> getAccounts(Context context);
+        private List<AccountInfo> getAccounts(Context context) {
+            final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
+            final List<AccountInfo> result = new ArrayList<>();
+            final List<AccountWithDataSet> sourceAccounts = getSourceAccounts(context);
+            for (AccountWithDataSet account : sourceAccounts) {
+                result.add(accountTypeManager.getAccountInfoForAccount(account));
+            }
+            return result;
+        }
+
+        public abstract List<AccountWithDataSet> getSourceAccounts(Context context);
     }
 
     public AccountsListAdapter(Context context, AccountListFilter filter) {
@@ -77,7 +83,7 @@
         this(context, filter.getAccounts(context), currentAccount);
     }
 
-    public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts) {
+    public AccountsListAdapter(Context context, List<AccountInfo> accounts) {
         this(context, accounts, null);
     }
 
@@ -85,22 +91,18 @@
      * @param currentAccount the Account currently selected by the user, which should come
      * first in the list. Can be null.
      */
-    public AccountsListAdapter(Context context, List<AccountWithDataSet> accounts,
+    public AccountsListAdapter(Context context, List<AccountInfo> accounts,
             AccountWithDataSet currentAccount) {
         mContext = context;
-        if (currentAccount != null
+
+        final AccountInfo currentInfo = AccountInfo.getAccount(accounts, currentAccount);
+        if (currentInfo != null
                 && !accounts.isEmpty()
-                && !accounts.get(0).equals(currentAccount)
-                && accounts.remove(currentAccount)) {
-            accounts.add(0, currentAccount);
+                && !accounts.get(0).sameAccount(currentAccount)
+                && accounts.remove(currentInfo)) {
+            accounts.add(0, currentInfo);
         }
 
-        final AccountDisplayInfoFactory factory = new AccountDisplayInfoFactory(context,
-                accounts);
-        mAccountDisplayInfoList = new ArrayList<>(accounts.size());
-        for (AccountWithDataSet account : accounts) {
-            mAccountDisplayInfoList.add(factory.getAccountDisplayInfo(account));
-        }
         mInflater = LayoutInflater.from(context);
 
         mAccounts = accounts;
@@ -120,22 +122,22 @@
         final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
         final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
 
-        text1.setText(mAccountDisplayInfoList.get(position).getTypeLabel());
-        text2.setText(mAccountDisplayInfoList.get(position).getNameLabel());
+        text1.setText(mAccounts.get(position).getTypeLabel());
+        text2.setText(mAccounts.get(position).getNameLabel());
 
-        icon.setImageDrawable(mAccountDisplayInfoList.get(position).getIcon());
+        icon.setImageDrawable(mAccounts.get(position).getIcon());
 
         return resultView;
     }
 
     @Override
     public int getCount() {
-        return mAccountDisplayInfoList.size();
+        return mAccounts.size();
     }
 
     @Override
     public AccountWithDataSet getItem(int position) {
-        return mAccountDisplayInfoList.get(position).getSource();
+        return mAccounts.get(position).getAccount();
     }
 
     @Override
diff --git a/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
index f7edb64..441ca68 100644
--- a/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
+++ b/src/com/android/contacts/util/concurrent/ListenableFutureLoader.java
@@ -18,7 +18,9 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.Loader;
+import android.support.v4.content.LocalBroadcastManager;
 import android.util.Log;
 
 import com.google.common.util.concurrent.FutureCallback;
@@ -38,9 +40,14 @@
 public abstract class ListenableFutureLoader<D> extends Loader<D> {
     private static final String TAG = "FutureLoader";
 
+    private final IntentFilter mReloadFilter;
+    private final Executor mUiExecutor;
+    private final LocalBroadcastManager mLocalBroadcastManager;
+
     private ListenableFuture<D> mFuture;
     private D mLoadedData;
-    private Executor mUiExecutor;
+
+    private BroadcastReceiver mReceiver;
 
     /**
      * Stores away the application context associated with context.
@@ -53,12 +60,23 @@
      * @param context used to retrieve the application context.
      */
     public ListenableFutureLoader(Context context) {
+        this(context, null);
+    }
+
+    public ListenableFutureLoader(Context context, IntentFilter reloadBroadcastFilter) {
         super(context);
         mUiExecutor = ContactsExecutors.newUiThreadExecutor();
+        mReloadFilter = reloadBroadcastFilter;
+        mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
     }
 
     @Override
     protected void onStartLoading() {
+        if (mReloadFilter != null && mReceiver == null) {
+            mReceiver = new ForceLoadReceiver();
+            mLocalBroadcastManager.registerReceiver(mReceiver, mReloadFilter);
+        }
+
         if (mLoadedData != null) {
             deliverResult(mLoadedData);
         }
@@ -76,8 +94,10 @@
         Futures.addCallback(mFuture, new FutureCallback<D>() {
             @Override
             public void onSuccess(D result) {
+                if (mLoadedData == null || !isSameData(mLoadedData, result)) {
+                    deliverResult(result);
+                }
                 mLoadedData = result;
-                deliverResult(mLoadedData);
                 commitContentChanged();
             }
 
@@ -105,10 +125,28 @@
     protected void onReset() {
         mFuture = null;
         mLoadedData = null;
+        if (mReceiver != null) {
+            mLocalBroadcastManager.unregisterReceiver(mReceiver);
+        }
     }
 
     protected abstract ListenableFuture<D> loadData();
 
+    /**
+     * Returns whether the newly loaded data is the same as the cached value
+     *
+     * <p>This allows subclasses to suppress delivering results when the data hasn't
+     * actually changed. By default it will always return false.
+     * </p>
+     */
+    protected boolean isSameData(D previousData, D newData) {
+        return false;
+    }
+
+    public final D getLoadedData() {
+        return mLoadedData;
+    }
+
     public class ForceLoadReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
diff --git a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
index e1c370a..956f775 100644
--- a/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/test/mocks/MockAccountTypeManager.java
@@ -18,6 +18,7 @@
 import android.accounts.Account;
 
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountInfo;
 import com.android.contacts.model.account.AccountType;
 import com.android.contacts.model.account.AccountTypeWithDataSet;
 import com.android.contacts.model.account.AccountWithDataSet;
@@ -70,19 +71,18 @@
     }
 
     @Override
-    public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
-        return Lists.newArrayList(Collections2.filter(Arrays.asList(mAccounts), filter));
+    public ListenableFuture<List<AccountInfo>> getAccountsAsync() {
+        throw new UnsupportedOperationException("not implemented");
     }
 
     @Override
-    public ListenableFuture<List<AccountWithDataSet>> getAllAccountsAsync() {
-        return Futures.immediateFuture(Arrays.asList(mAccounts));
+    public ListenableFuture<List<AccountInfo>> filterAccountsAsync(Predicate<AccountInfo> filter) {
+        throw new UnsupportedOperationException("not implemented");
     }
 
     @Override
-    public ListenableFuture<List<AccountWithDataSet>> filterAccountsByTypeAsync(
-            Predicate<AccountType> type) {
-        return Futures.immediateFuture(Arrays.asList(mAccounts));
+    public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) {
+        throw new UnsupportedOperationException("not implemented");
     }
 
     @Override