Reduce the flickering of injected items when package is changed am: e4b2b77452

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/12771880

Change-Id: Ib2ff68b59e8aa9567d1f6f7c7281832dd323c2dc
diff --git a/src/com/android/settings/core/SettingsBaseActivity.java b/src/com/android/settings/core/SettingsBaseActivity.java
index 57697a6..199034c 100644
--- a/src/com/android/settings/core/SettingsBaseActivity.java
+++ b/src/com/android/settings/core/SettingsBaseActivity.java
@@ -41,11 +41,14 @@
 import com.android.settings.R;
 import com.android.settings.SubSettings;
 import com.android.settings.dashboard.CategoryManager;
+import com.android.settingslib.drawer.Tile;
 
 import com.google.android.setupcompat.util.WizardManagerHelper;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 public class SettingsBaseActivity extends FragmentActivity {
 
@@ -59,6 +62,7 @@
 
     private final PackageReceiver mPackageReceiver = new PackageReceiver();
     private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
+    private int mCategoriesUpdateTaskCount;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -147,10 +151,10 @@
         ((ViewGroup) findViewById(R.id.content_frame)).addView(view, params);
     }
 
-    private void onCategoriesChanged() {
+    private void onCategoriesChanged(Set<String> categories) {
         final int N = mCategoryListeners.size();
         for (int i = 0; i < N; i++) {
-            mCategoryListeners.get(i).onCategoriesChanged();
+            mCategoryListeners.get(i).onCategoriesChanged(categories);
         }
     }
 
@@ -194,38 +198,100 @@
      * Updates dashboard categories. Only necessary to call this after setTileEnabled
      */
     public void updateCategories() {
-        new CategoriesUpdateTask().execute();
+        updateCategories(false /* fromBroadcast */);
+    }
+
+    private void updateCategories(boolean fromBroadcast) {
+        // Only allow at most 2 tasks existing at the same time since when the first one is
+        // executing, there may be new data from the second update request.
+        // Ignore the third update request because the second task is still waiting for the first
+        // task to complete in a serial thread, which will get the latest data.
+        if (mCategoriesUpdateTaskCount < 2) {
+            new CategoriesUpdateTask().execute(fromBroadcast);
+        }
     }
 
     public interface CategoryListener {
-        void onCategoriesChanged();
+        /**
+         * @param categories the changed categories that have to be refreshed, or null to force
+         * refreshing all.
+         */
+        void onCategoriesChanged(@Nullable Set<String> categories);
     }
 
-    private class CategoriesUpdateTask extends AsyncTask<Void, Void, Void> {
+    private class CategoriesUpdateTask extends AsyncTask<Boolean, Void, Set<String>> {
 
+        private final Context mContext;
         private final CategoryManager mCategoryManager;
+        private Map<ComponentName, Tile> mPreviousTileMap;
 
         public CategoriesUpdateTask() {
-            mCategoryManager = CategoryManager.get(SettingsBaseActivity.this);
+            mCategoriesUpdateTaskCount++;
+            mContext = SettingsBaseActivity.this;
+            mCategoryManager = CategoryManager.get(mContext);
         }
 
         @Override
-        protected Void doInBackground(Void... params) {
-            mCategoryManager.reloadAllCategories(SettingsBaseActivity.this);
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
+        protected Set<String> doInBackground(Boolean... params) {
+            mPreviousTileMap = mCategoryManager.getTileByComponentMap();
+            mCategoryManager.reloadAllCategories(mContext);
             mCategoryManager.updateCategoryFromBlacklist(sTileBlacklist);
-            onCategoriesChanged();
+            return getChangedCategories(params[0]);
+        }
+
+        @Override
+        protected void onPostExecute(Set<String> categories) {
+            if (categories == null || !categories.isEmpty()) {
+                onCategoriesChanged(categories);
+            }
+            mCategoriesUpdateTaskCount--;
+        }
+
+        // Return the changed categories that have to be refreshed, or null to force refreshing all.
+        private Set<String> getChangedCategories(boolean fromBroadcast) {
+            if (!fromBroadcast) {
+                // Always refresh for non-broadcast case.
+                return null;
+            }
+
+            final Set<String> changedCategories = new ArraySet<>();
+            final Map<ComponentName, Tile> currentTileMap =
+                    mCategoryManager.getTileByComponentMap();
+            currentTileMap.forEach((component, currentTile) -> {
+                final Tile previousTile = mPreviousTileMap.get(component);
+                // Check if the tile is newly added.
+                if (previousTile == null) {
+                    Log.i(TAG, "Tile added: " + component.flattenToShortString());
+                    changedCategories.add(currentTile.getCategory());
+                    return;
+                }
+
+                // Check if the title or summary has changed.
+                if (!TextUtils.equals(currentTile.getTitle(mContext),
+                        previousTile.getTitle(mContext))
+                        || !TextUtils.equals(currentTile.getSummary(mContext),
+                        previousTile.getSummary(mContext))) {
+                    Log.i(TAG, "Tile changed: " + component.flattenToShortString());
+                    changedCategories.add(currentTile.getCategory());
+                }
+            });
+
+            // Check if any previous tile is removed.
+            final Set<ComponentName> removal = new ArraySet(mPreviousTileMap.keySet());
+            removal.removeAll(currentTileMap.keySet());
+            removal.forEach(component -> {
+                Log.i(TAG, "Tile removed: " + component.flattenToShortString());
+                changedCategories.add(mPreviousTileMap.get(component).getCategory());
+            });
+
+            return changedCategories;
         }
     }
 
     private class PackageReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
-            updateCategories();
+            updateCategories(true /* fromBroadcast */);
         }
     }
 }
diff --git a/src/com/android/settings/dashboard/CategoryManager.java b/src/com/android/settings/dashboard/CategoryManager.java
index 525b6f8..b66de9d 100644
--- a/src/com/android/settings/dashboard/CategoryManager.java
+++ b/src/com/android/settings/dashboard/CategoryManager.java
@@ -41,6 +41,7 @@
 public class CategoryManager {
 
     private static final String TAG = "CategoryManager";
+    private static final boolean DEBUG = false;
 
     private static CategoryManager sInstance;
     private final InterestingConfigChanges mInterestingConfigChanges;
@@ -88,6 +89,7 @@
     public synchronized void updateCategoryFromBlacklist(Set<ComponentName> tileBlacklist) {
         if (mCategories == null) {
             Log.w(TAG, "Category is null, skipping blacklist update");
+            return;
         }
         for (int i = 0; i < mCategories.size(); i++) {
             DashboardCategory category = mCategories.get(i);
@@ -100,6 +102,31 @@
         }
     }
 
+    /** Return the current tile map */
+    public synchronized Map<ComponentName, Tile> getTileByComponentMap() {
+        final Map<ComponentName, Tile> result = new ArrayMap<>();
+        if (mCategories == null) {
+            Log.w(TAG, "Category is null, no tiles");
+            return result;
+        }
+        mCategories.forEach(category -> {
+            for (int i = 0; i < category.getTilesCount(); i++) {
+                final Tile tile = category.getTile(i);
+                result.put(tile.getIntent().getComponent(), tile);
+            }
+        });
+        return result;
+    }
+
+    private void logTiles(Context context) {
+        if (DEBUG) {
+            getTileByComponentMap().forEach((component, tile) -> {
+                Log.d(TAG, "Tile: " + tile.getCategory().replace("com.android.settings.", "")
+                        + ": " + tile.getTitle(context) + ", " + component.flattenToShortString());
+            });
+        }
+    }
+
     private synchronized void tryInitCategories(Context context) {
         // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
         // happens.
@@ -108,6 +135,7 @@
 
     private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
         if (mCategories == null) {
+            final boolean firstLoading = mCategoryByKeyMap.isEmpty();
             if (forceClearCache) {
                 mTileByComponentCache.clear();
             }
@@ -119,6 +147,9 @@
             backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
             sortCategories(context, mCategoryByKeyMap);
             filterDuplicateTiles(mCategoryByKeyMap);
+            if (firstLoading) {
+                logTiles(context);
+            }
         }
     }
 
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index 8084038..69f1f1b 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -56,6 +56,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
 /**
@@ -160,13 +161,21 @@
     }
 
     @Override
-    public void onCategoriesChanged() {
-        final DashboardCategory category =
-                mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
-        if (category == null) {
+    public void onCategoriesChanged(Set<String> categories) {
+        final String categoryKey = getCategoryKey();
+        final DashboardCategory dashboardCategory =
+                mDashboardFeatureProvider.getTilesForCategory(categoryKey);
+        if (dashboardCategory == null) {
             return;
         }
-        refreshDashboardTiles(getLogTag());
+
+        if (categories == null) {
+            // force refreshing
+            refreshDashboardTiles(getLogTag());
+        } else if (categories.contains(categoryKey)) {
+            Log.i(TAG, "refresh tiles for " + categoryKey);
+            refreshDashboardTiles(getLogTag());
+        }
     }
 
     @Override