Only broadcast account change if accounts are different

Test: manually verify accounts update in editor account chooser when
added and removed

Bug 33627801
Change-Id: I5e0bf85ff5b513df66462cb35dd2ed5db2bc5baa
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 961fa63..fa8d6e2 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -52,6 +52,7 @@
 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;
@@ -333,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();
         }
     };
@@ -426,15 +431,26 @@
 
     @Override
     public void onStatusChanged(int which) {
-        reloadAccountTypes();
+        reloadAccountTypesIfNeeded();
     }
 
     /* 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));
@@ -442,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() {
@@ -476,9 +516,10 @@
         });
     }
 
-    private void reloadLocalAccounts() {
+    private synchronized void reloadLocalAccounts() {
         loadLocalAccounts();
-        Futures.addCallback(mLocalAccountsFuture, mAccountsUpdateCallback, mMainThreadExecutor);
+        Futures.addCallback(mLocalAccountsFuture, newAccountsUpdatedCallback(mLocalAccounts),
+                mMainThreadExecutor);
     }
 
     /**
@@ -499,52 +540,44 @@
         return getAllAccountsAsyncInternal();
     }
 
-    private ListenableFuture<List<AccountInfo>> getAllAccountsAsyncInternal() {
+    private synchronized ListenableFuture<List<AccountInfo>> getAllAccountsAsyncInternal() {
         startLoadingIfNeeded();
         final AccountTypeProvider typeProvider = mTypeProvider;
-        final ListenableFuture<List<AccountInfo>> accountsFromTypes =
-                Futures.transform(Futures.nonCancellationPropagating(mAccountTypesFuture),
-                        new Function<AccountTypeProvider, List<AccountInfo>>() {
-                            @Override
-                            public List<AccountInfo> apply(AccountTypeProvider provider) {
-                                return getAccountsFromProvider(provider);
-                            }
-                        });
+        final ListenableFuture<List<List<AccountWithDataSet>>> all =
+                Futures.nonCancellationPropagating(
+                        Futures.successfulAsList(
+                                Futures.transform(mAccountTypesFuture, mAccountsExtractor),
+                                mLocalAccountsFuture));
 
-        final ListenableFuture<List<AccountInfo>> localAccountsInfo =
-                Futures.transform(mLocalAccountsFuture, new Function<List<AccountWithDataSet>,
-                        List<AccountInfo>>() {
-                    @Nullable
-                    @Override
-                    public List<AccountInfo> apply(@Nullable List<AccountWithDataSet> input) {
-                        final List<AccountInfo> result = new ArrayList<>();
-                        for (AccountWithDataSet account : input) {
-                            final AccountType type = typeProvider.getTypeForAccount(account);
-                            result.add(type.wrapAccount(mContext, account));
-                        }
-                        return result;
-                    }
-                });
-
-        final ListenableFuture<List<List<AccountInfo>>> all =
-                Futures.successfulAsList(accountsFromTypes, localAccountsInfo);
-
-        return Futures.transform(all, new Function<List<List<AccountInfo>>,
+        return Futures.transform(all, new Function<List<List<AccountWithDataSet>>,
                 List<AccountInfo>>() {
+            @Nullable
             @Override
-            public List<AccountInfo> apply(List<List<AccountInfo>> input) {
+            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<>(input.get(0));
-                // Check if there is a Google account in this list and if there is exclude the null
-                // account
-                if (hasWritableGoogleAccount(input.get(0))) {
-                    result.addAll(Collections2.filter(input.get(1), nonNullAccountFilter()));
-                } else {
-                    result.addAll(input.get(1));
+                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;
@@ -573,32 +606,19 @@
         return type.wrapAccount(mContext, account);
     }
 
-    private List<AccountInfo> getAccountsFromProvider(AccountTypeProvider typeProvider) {
-        final List<AccountInfo> result = new ArrayList<>();
-        final Account[] accounts = mAccountManager.getAccounts();
+    private List<AccountWithDataSet> getAccountsWithDataSets(Account[] accounts,
+            AccountTypeProvider typeProvider) {
+        List<AccountWithDataSet> result = new ArrayList<>();
         for (Account account : accounts) {
             final List<AccountType> types = typeProvider.getAccountTypes(account.type);
             for (AccountType type : types) {
-                result.add(type.wrapAccount(mContext,
-                        new AccountWithDataSet(account.name, account.type, type.dataSet)));
+                result.add(new AccountWithDataSet(
+                        account.name, account.type, type.dataSet));
             }
         }
         return result;
     }
 
-    private boolean hasWritableGoogleAccount(List<AccountInfo> accounts) {
-        if (accounts == null) {
-            return false;
-        }
-        for (AccountInfo info : accounts) {
-            AccountWithDataSet account = info.getAccount();
-            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.
      */
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;
+    }
+
 }