diff --git a/src/com/android/settings/core/SettingsBaseActivity.java b/src/com/android/settings/core/SettingsBaseActivity.java index 967269476ad..23eb860d306 100644 --- a/src/com/android/settings/core/SettingsBaseActivity.java +++ b/src/com/android/settings/core/SettingsBaseActivity.java @@ -38,7 +38,7 @@ import android.widget.Toolbar; import androidx.fragment.app.FragmentActivity; import com.android.settings.R; -import com.android.settingslib.drawer.CategoryManager; +import com.android.settings.dashboard.CategoryManager; import java.util.ArrayList; import java.util.List; @@ -172,10 +172,6 @@ public class SettingsBaseActivity extends FragmentActivity { new CategoriesUpdateTask().execute(); } - public String getSettingPkg() { - return CategoryManager.SETTING_PKG; - } - public interface CategoryListener { void onCategoriesChanged(); } @@ -190,7 +186,7 @@ public class SettingsBaseActivity extends FragmentActivity { @Override protected Void doInBackground(Void... params) { - mCategoryManager.reloadAllCategories(SettingsBaseActivity.this, getSettingPkg()); + mCategoryManager.reloadAllCategories(SettingsBaseActivity.this); return null; } diff --git a/src/com/android/settings/dashboard/CategoryManager.java b/src/com/android/settings/dashboard/CategoryManager.java new file mode 100644 index 00000000000..f0004583444 --- /dev/null +++ b/src/com/android/settings/dashboard/CategoryManager.java @@ -0,0 +1,232 @@ +/* + * 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.settings.dashboard; + +import android.content.ComponentName; +import android.content.Context; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.applications.InterestingConfigChanges; +import com.android.settingslib.drawer.CategoryKey; +import com.android.settingslib.drawer.DashboardCategory; +import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class CategoryManager { + + public static final String SETTING_PKG = "com.android.settings"; + + private static final String TAG = "CategoryManager"; + + private static CategoryManager sInstance; + private final InterestingConfigChanges mInterestingConfigChanges; + + // Tile cache (key: , value: tile) + private final Map, Tile> mTileByComponentCache; + + // Tile cache (key: category key, value: category) + private final Map mCategoryByKeyMap; + + private List mCategories; + private String mExtraAction; + + public static CategoryManager get(Context context) { + return get(context, null); + } + + public static CategoryManager get(Context context, String action) { + if (sInstance == null) { + sInstance = new CategoryManager(context, action); + } + return sInstance; + } + + CategoryManager(Context context, String action) { + mTileByComponentCache = new ArrayMap<>(); + mCategoryByKeyMap = new ArrayMap<>(); + mInterestingConfigChanges = new InterestingConfigChanges(); + mInterestingConfigChanges.applyNewConfig(context.getResources()); + mExtraAction = action; + } + + public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) { + tryInitCategories(context); + + return mCategoryByKeyMap.get(categoryKey); + } + + public synchronized List getCategories(Context context) { + tryInitCategories(context); + return mCategories; + } + + public synchronized void reloadAllCategories(Context context) { + final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig( + context.getResources()); + mCategories = null; + tryInitCategories(context, forceClearCache); + } + + public synchronized void updateCategoryFromBlacklist(Set tileBlacklist) { + if (mCategories == null) { + Log.w(TAG, "Category is null, skipping blacklist update"); + } + for (int i = 0; i < mCategories.size(); i++) { + DashboardCategory category = mCategories.get(i); + for (int j = 0; j < category.getTilesCount(); j++) { + Tile tile = category.getTile(j); + if (tileBlacklist.contains(tile.intent.getComponent())) { + category.removeTile(j--); + } + } + } + } + + private synchronized void tryInitCategories(Context context) { + // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange + // happens. + tryInitCategories(context, false /* forceClearCache */); + } + + private synchronized void tryInitCategories(Context context, boolean forceClearCache) { + if (mCategories == null) { + if (forceClearCache) { + mTileByComponentCache.clear(); + } + mCategoryByKeyMap.clear(); + mCategories = TileUtils.getCategories(context, mTileByComponentCache, mExtraAction); + for (DashboardCategory category : mCategories) { + mCategoryByKeyMap.put(category.key, category); + } + backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); + sortCategories(context, mCategoryByKeyMap); + filterDuplicateTiles(mCategoryByKeyMap); + } + } + + @VisibleForTesting + synchronized void backwardCompatCleanupForCategory( + Map, Tile> tileByComponentCache, + Map categoryByKeyMap) { + // A package can use a) CategoryKey, b) old category keys, c) both. + // Check if a package uses old category key only. + // If yes, map them to new category key. + + // Build a package name -> tile map first. + final Map> packageToTileMap = new HashMap<>(); + for (Entry, Tile> tileEntry : tileByComponentCache.entrySet()) { + final String packageName = tileEntry.getKey().first; + List tiles = packageToTileMap.get(packageName); + if (tiles == null) { + tiles = new ArrayList<>(); + packageToTileMap.put(packageName, tiles); + } + tiles.add(tileEntry.getValue()); + } + + for (Entry> entry : packageToTileMap.entrySet()) { + final List tiles = entry.getValue(); + // Loop map, find if all tiles from same package uses old key only. + boolean useNewKey = false; + boolean useOldKey = false; + for (Tile tile : tiles) { + if (CategoryKey.KEY_COMPAT_MAP.containsKey(tile.category)) { + useOldKey = true; + } else { + useNewKey = true; + break; + } + } + // Uses only old key, map them to new keys one by one. + if (useOldKey && !useNewKey) { + for (Tile tile : tiles) { + final String newCategoryKey = CategoryKey.KEY_COMPAT_MAP.get(tile.category); + tile.category = newCategoryKey; + // move tile to new category. + DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey); + if (newCategory == null) { + newCategory = new DashboardCategory(); + categoryByKeyMap.put(newCategoryKey, newCategory); + } + newCategory.addTile(tile); + } + } + } + } + + /** + * Sort the tiles injected from all apps such that if they have the same priority value, + * they wil lbe sorted by package name. + *

+ * A list of tiles are considered sorted when their priority value decreases in a linear + * scan. + */ + @VisibleForTesting + synchronized void sortCategories(Context context, + Map categoryByKeyMap) { + for (Entry categoryEntry : categoryByKeyMap.entrySet()) { + categoryEntry.getValue().sortTiles(context.getPackageName()); + } + } + + /** + * Filter out duplicate tiles from category. Duplicate tiles are the ones pointing to the + * same intent. + */ + @VisibleForTesting + synchronized void filterDuplicateTiles(Map categoryByKeyMap) { + for (Entry categoryEntry : categoryByKeyMap.entrySet()) { + final DashboardCategory category = categoryEntry.getValue(); + final int count = category.getTilesCount(); + final Set components = new ArraySet<>(); + for (int i = count - 1; i >= 0; i--) { + final Tile tile = category.getTile(i); + if (tile.intent == null) { + continue; + } + final ComponentName tileComponent = tile.intent.getComponent(); + if (components.contains(tileComponent)) { + category.removeTile(i); + } else { + components.add(tileComponent); + } + } + } + } + + /** + * Sort priority value for tiles within a single {@code DashboardCategory}. + * + * @see #sortCategories(Context, Map) + */ + private synchronized void sortCategoriesForExternalTiles(Context context, + DashboardCategory dashboardCategory) { + dashboardCategory.sortTiles(context.getPackageName()); + + } +} diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index a44335518e9..46beac4af47 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -40,7 +40,6 @@ import com.android.settings.SettingsActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; -import com.android.settingslib.drawer.CategoryManager; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.ProfileSelectDialog; import com.android.settingslib.drawer.Tile; diff --git a/tests/robotests/src/com/android/settings/dashboard/CategoryManagerTest.java b/tests/robotests/src/com/android/settings/dashboard/CategoryManagerTest.java new file mode 100644 index 00000000000..e22f07d0ec7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/dashboard/CategoryManagerTest.java @@ -0,0 +1,345 @@ +/* + * 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.settings.dashboard; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Pair; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.drawer.CategoryKey; +import com.android.settingslib.drawer.DashboardCategory; +import com.android.settingslib.drawer.Tile; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowApplication; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(SettingsRobolectricTestRunner.class) +public class CategoryManagerTest { + + private Context mContext; + private CategoryManager mCategoryManager; + private Map, Tile> mTileByComponentCache; + private Map mCategoryByKeyMap; + + @Before + public void setUp() { + mContext = ShadowApplication.getInstance().getApplicationContext(); + mTileByComponentCache = new HashMap<>(); + mCategoryByKeyMap = new HashMap<>(); + mCategoryManager = CategoryManager.get(mContext); + } + + @Test + public void getInstance_shouldBeSingleton() { + assertThat(mCategoryManager).isSameAs(CategoryManager.get(mContext)); + } + + @Test + public void backwardCompatCleanupForCategory_shouldNotChangeCategoryForNewKeys() { + final Tile tile1 = new Tile(); + final Tile tile2 = new Tile(); + tile1.category = CategoryKey.CATEGORY_ACCOUNT; + tile2.category = CategoryKey.CATEGORY_ACCOUNT; + final DashboardCategory category = new DashboardCategory(); + category.addTile(tile1); + category.addTile(tile2); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_ACCOUNT, category); + mTileByComponentCache.put(new Pair<>("PACKAGE", "1"), tile1); + mTileByComponentCache.put(new Pair<>("PACKAGE", "2"), tile2); + + mCategoryManager.backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); + + assertThat(mCategoryByKeyMap.size()).isEqualTo(1); + assertThat(mCategoryByKeyMap.get(CategoryKey.CATEGORY_ACCOUNT)).isNotNull(); + } + + @Test + public void backwardCompatCleanupForCategory_shouldNotChangeCategoryForMixedKeys() { + final Tile tile1 = new Tile(); + final Tile tile2 = new Tile(); + final String oldCategory = "com.android.settings.category.wireless"; + tile1.category = CategoryKey.CATEGORY_ACCOUNT; + tile2.category = oldCategory; + final DashboardCategory category1 = new DashboardCategory(); + category1.addTile(tile1); + final DashboardCategory category2 = new DashboardCategory(); + category2.addTile(tile2); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_ACCOUNT, category1); + mCategoryByKeyMap.put(oldCategory, category2); + mTileByComponentCache.put(new Pair<>("PACKAGE", "CLASS1"), tile1); + mTileByComponentCache.put(new Pair<>("PACKAGE", "CLASS2"), tile2); + + mCategoryManager.backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); + + assertThat(mCategoryByKeyMap.size()).isEqualTo(2); + assertThat( + mCategoryByKeyMap.get(CategoryKey.CATEGORY_ACCOUNT).getTilesCount()).isEqualTo(1); + assertThat(mCategoryByKeyMap.get(oldCategory).getTilesCount()).isEqualTo(1); + } + + @Test + public void backwardCompatCleanupForCategory_shouldChangeCategoryForOldKeys() { + final Tile tile1 = new Tile(); + final String oldCategory = "com.android.settings.category.wireless"; + tile1.category = oldCategory; + final DashboardCategory category1 = new DashboardCategory(); + category1.addTile(tile1); + mCategoryByKeyMap.put(oldCategory, category1); + mTileByComponentCache.put(new Pair<>("PACKAGE", "CLASS1"), tile1); + + mCategoryManager.backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); + + // Added 1 more category to category map. + assertThat(mCategoryByKeyMap.size()).isEqualTo(2); + // The new category map has CATEGORY_NETWORK type now, which contains 1 tile. + assertThat( + mCategoryByKeyMap.get(CategoryKey.CATEGORY_NETWORK).getTilesCount()).isEqualTo(1); + // Old category still exists. + assertThat(mCategoryByKeyMap.get(oldCategory).getTilesCount()).isEqualTo(1); + } + + @Test + public void sortCategories_singlePackage_shouldReorderBasedOnPriority() { + // Create some fake tiles that are not sorted. + final String testPackage = "com.android.test"; + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile1.priority = 100; + final Tile tile2 = new Tile(); + tile2.intent = + new Intent().setComponent(new ComponentName(testPackage, "class2")); + tile2.priority = 50; + final Tile tile3 = new Tile(); + tile3.intent = + new Intent().setComponent(new ComponentName(testPackage, "class3")); + tile3.priority = 200; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + // Sort their priorities + mCategoryManager.sortCategories(ShadowApplication.getInstance().getApplicationContext(), + mCategoryByKeyMap); + + // Verify they are now sorted. + assertThat(category.getTile(0)).isSameAs(tile3); + assertThat(category.getTile(1)).isSameAs(tile1); + assertThat(category.getTile(2)).isSameAs(tile2); + } + + @Test + public void sortCategories_multiPackage_shouldReorderBasedOnPackageAndPriority() { + // Create some fake tiles that are not sorted. + final String testPackage1 = "com.android.test1"; + final String testPackage2 = "com.android.test2"; + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = + new Intent().setComponent(new ComponentName(testPackage2, "class1")); + tile1.priority = 100; + final Tile tile2 = new Tile(); + tile2.intent = + new Intent().setComponent(new ComponentName(testPackage1, "class2")); + tile2.priority = 100; + final Tile tile3 = new Tile(); + tile3.intent = + new Intent().setComponent(new ComponentName(testPackage1, "class3")); + tile3.priority = 50; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + // Sort their priorities + mCategoryManager.sortCategories(ShadowApplication.getInstance().getApplicationContext(), + mCategoryByKeyMap); + + // Verify they are now sorted. + assertThat(category.getTile(0)).isSameAs(tile2); + assertThat(category.getTile(1)).isSameAs(tile1); + assertThat(category.getTile(2)).isSameAs(tile3); + } + + @Test + public void sortCategories_internalPackageTiles_shouldSkipTileForInternalPackage() { + // Create some fake tiles that are not sorted. + final String testPackage = + ShadowApplication.getInstance().getApplicationContext().getPackageName(); + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile1.priority = 100; + final Tile tile2 = new Tile(); + tile2.intent = + new Intent().setComponent(new ComponentName(testPackage, "class2")); + tile2.priority = 100; + final Tile tile3 = new Tile(); + tile3.intent = + new Intent().setComponent(new ComponentName(testPackage, "class3")); + tile3.priority = 50; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + // Sort their priorities + mCategoryManager.sortCategories(ShadowApplication.getInstance().getApplicationContext(), + mCategoryByKeyMap); + + // Verify the sorting order is not changed + assertThat(category.getTile(0)).isSameAs(tile1); + assertThat(category.getTile(1)).isSameAs(tile2); + assertThat(category.getTile(2)).isSameAs(tile3); + } + + @Test + public void sortCategories_internalAndExternalPackageTiles_shouldRetainPriorityOrdering() { + // Inject one external tile among internal tiles. + final String testPackage = + ShadowApplication.getInstance().getApplicationContext().getPackageName(); + final String testPackage2 = "com.google.test2"; + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile1.priority = 2; + final Tile tile2 = new Tile(); + tile2.intent = new Intent().setComponent(new ComponentName(testPackage, "class2")); + tile2.priority = 1; + final Tile tile3 = new Tile(); + tile3.intent = new Intent().setComponent(new ComponentName(testPackage2, "class0")); + tile3.priority = 0; + final Tile tile4 = new Tile(); + tile4.intent = new Intent().setComponent(new ComponentName(testPackage, "class3")); + tile4.priority = -1; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + category.addTile(tile4); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + // Sort their priorities + mCategoryManager.sortCategories(ShadowApplication.getInstance().getApplicationContext(), + mCategoryByKeyMap); + + // Verify the sorting order is not changed + assertThat(category.getTile(0)).isSameAs(tile1); + assertThat(category.getTile(1)).isSameAs(tile2); + assertThat(category.getTile(2)).isSameAs(tile3); + assertThat(category.getTile(3)).isSameAs(tile4); + } + + @Test + public void sortCategories_samePriority_internalPackageTileShouldTakePrecedence() { + // Inject one external tile among internal tiles with same priority. + final String testPackage = + ShadowApplication.getInstance().getApplicationContext().getPackageName(); + final String testPackage2 = "com.google.test2"; + final String testPackage3 = "com.abcde.test3"; + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = new Intent().setComponent(new ComponentName(testPackage2, "class1")); + tile1.priority = 1; + final Tile tile2 = new Tile(); + tile2.intent = new Intent().setComponent(new ComponentName(testPackage, "class2")); + tile2.priority = 1; + final Tile tile3 = new Tile(); + tile3.intent = new Intent().setComponent(new ComponentName(testPackage3, "class3")); + tile3.priority = 1; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + // Sort their priorities + mCategoryManager.sortCategories(ShadowApplication.getInstance().getApplicationContext(), + mCategoryByKeyMap); + + // Verify the sorting order is internal first, follow by package name ordering + assertThat(category.getTile(0)).isSameAs(tile2); + assertThat(category.getTile(1)).isSameAs(tile3); + assertThat(category.getTile(2)).isSameAs(tile1); + } + + @Test + public void filterTiles_noDuplicate_noChange() { + // Create some unique tiles + final String testPackage = + ShadowApplication.getInstance().getApplicationContext().getPackageName(); + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile1.priority = 100; + final Tile tile2 = new Tile(); + tile2.intent = + new Intent().setComponent(new ComponentName(testPackage, "class2")); + tile2.priority = 100; + final Tile tile3 = new Tile(); + tile3.intent = + new Intent().setComponent(new ComponentName(testPackage, "class3")); + tile3.priority = 50; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + mCategoryManager.filterDuplicateTiles(mCategoryByKeyMap); + + assertThat(category.getTilesCount()).isEqualTo(3); + } + + @Test + public void filterTiles_hasDuplicate_shouldOnlyKeepUniqueTiles() { + // Create tiles pointing to same intent. + final String testPackage = + ShadowApplication.getInstance().getApplicationContext().getPackageName(); + final DashboardCategory category = new DashboardCategory(); + final Tile tile1 = new Tile(); + tile1.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile1.priority = 100; + final Tile tile2 = new Tile(); + tile2.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile2.priority = 100; + final Tile tile3 = new Tile(); + tile3.intent = + new Intent().setComponent(new ComponentName(testPackage, "class1")); + tile3.priority = 50; + category.addTile(tile1); + category.addTile(tile2); + category.addTile(tile3); + mCategoryByKeyMap.put(CategoryKey.CATEGORY_HOMEPAGE, category); + + mCategoryManager.filterDuplicateTiles(mCategoryByKeyMap); + + assertThat(category.getTilesCount()).isEqualTo(1); + } +} diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java index bf1e0ff6c0b..e541b9fc953 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java @@ -53,7 +53,6 @@ import com.android.settings.testutils.shadow.ShadowTileUtils; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; import com.android.settingslib.drawer.CategoryKey; -import com.android.settingslib.drawer.CategoryManager; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.TileUtils;