1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settings.dashboard; 17 18 import android.content.ComponentName; 19 import android.content.Context; 20 import android.util.ArrayMap; 21 import android.util.ArraySet; 22 import android.util.Log; 23 import android.util.Pair; 24 25 import androidx.annotation.VisibleForTesting; 26 27 import com.android.settingslib.applications.InterestingConfigChanges; 28 import com.android.settingslib.drawer.CategoryKey; 29 import com.android.settingslib.drawer.DashboardCategory; 30 import com.android.settingslib.drawer.ProviderTile; 31 import com.android.settingslib.drawer.Tile; 32 import com.android.settingslib.drawer.TileUtils; 33 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Map.Entry; 39 import java.util.Set; 40 41 public class CategoryManager { 42 43 private static final String TAG = "CategoryManager"; 44 private static final boolean DEBUG = false; 45 46 private static CategoryManager sInstance; 47 private final InterestingConfigChanges mInterestingConfigChanges; 48 49 // Tile cache (key: <packageName, activityName>, value: tile) 50 private final Map<Pair<String, String>, Tile> mTileByComponentCache; 51 52 // Tile cache (key: category key, value: category) 53 private final Map<String, DashboardCategory> mCategoryByKeyMap; 54 55 private List<DashboardCategory> mCategories; 56 get(Context context)57 public static CategoryManager get(Context context) { 58 if (sInstance == null) { 59 sInstance = new CategoryManager(context); 60 } 61 return sInstance; 62 } 63 CategoryManager(Context context)64 CategoryManager(Context context) { 65 mTileByComponentCache = new ArrayMap<>(); 66 mCategoryByKeyMap = new ArrayMap<>(); 67 mInterestingConfigChanges = new InterestingConfigChanges(); 68 mInterestingConfigChanges.applyNewConfig(context.getResources()); 69 } 70 getTilesByCategory(Context context, String categoryKey)71 public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) { 72 tryInitCategories(context); 73 74 return mCategoryByKeyMap.get(categoryKey); 75 } 76 getCategories(Context context)77 public synchronized List<DashboardCategory> getCategories(Context context) { 78 tryInitCategories(context); 79 return mCategories; 80 } 81 reloadAllCategories(Context context)82 public synchronized void reloadAllCategories(Context context) { 83 final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig( 84 context.getResources()); 85 mCategories = null; 86 tryInitCategories(context, forceClearCache); 87 } 88 89 /** 90 * Update category from deny list 91 * @param tileDenylist 92 */ updateCategoryFromDenylist(Set<ComponentName> tileDenylist)93 public synchronized void updateCategoryFromDenylist(Set<ComponentName> tileDenylist) { 94 if (mCategories == null) { 95 Log.w(TAG, "Category is null, skipping denylist update"); 96 return; 97 } 98 for (int i = 0; i < mCategories.size(); i++) { 99 DashboardCategory category = mCategories.get(i); 100 for (int j = 0; j < category.getTilesCount(); j++) { 101 Tile tile = category.getTile(j); 102 if (tileDenylist.contains(tile.getIntent().getComponent())) { 103 category.removeTile(j--); 104 } 105 } 106 } 107 } 108 109 /** Return the current tile map */ getTileByComponentMap()110 public synchronized Map<ComponentName, Tile> getTileByComponentMap() { 111 final Map<ComponentName, Tile> result = new ArrayMap<>(); 112 if (mCategories == null) { 113 Log.w(TAG, "Category is null, no tiles"); 114 return result; 115 } 116 mCategories.forEach(category -> { 117 for (int i = 0; i < category.getTilesCount(); i++) { 118 final Tile tile = category.getTile(i); 119 result.put(tile.getIntent().getComponent(), tile); 120 } 121 }); 122 return result; 123 } 124 logTiles(Context context)125 private void logTiles(Context context) { 126 if (DEBUG) { 127 getTileByComponentMap().forEach((component, tile) -> { 128 Log.d(TAG, "Tile: " + tile.getCategory().replace("com.android.settings.", "") 129 + ": " + tile.getTitle(context) + ", " + component.flattenToShortString()); 130 }); 131 } 132 } 133 tryInitCategories(Context context)134 private synchronized void tryInitCategories(Context context) { 135 // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange 136 // happens. 137 tryInitCategories(context, false /* forceClearCache */); 138 } 139 tryInitCategories(Context context, boolean forceClearCache)140 private synchronized void tryInitCategories(Context context, boolean forceClearCache) { 141 if (mCategories == null) { 142 final boolean firstLoading = mCategoryByKeyMap.isEmpty(); 143 if (forceClearCache) { 144 mTileByComponentCache.clear(); 145 } 146 mCategoryByKeyMap.clear(); 147 mCategories = TileUtils.getCategories(context, mTileByComponentCache); 148 for (DashboardCategory category : mCategories) { 149 mCategoryByKeyMap.put(category.key, category); 150 } 151 backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); 152 sortCategories(context, mCategoryByKeyMap); 153 filterDuplicateTiles(mCategoryByKeyMap); 154 if (firstLoading) { 155 logTiles(context); 156 } 157 } 158 } 159 160 @VisibleForTesting backwardCompatCleanupForCategory( Map<Pair<String, String>, Tile> tileByComponentCache, Map<String, DashboardCategory> categoryByKeyMap)161 synchronized void backwardCompatCleanupForCategory( 162 Map<Pair<String, String>, Tile> tileByComponentCache, 163 Map<String, DashboardCategory> categoryByKeyMap) { 164 // A package can use a) CategoryKey, b) old category keys, c) both. 165 // Check if a package uses old category key only. 166 // If yes, map them to new category key. 167 168 // Build a package name -> tile map first. 169 final Map<String, List<Tile>> packageToTileMap = new HashMap<>(); 170 for (Entry<Pair<String, String>, Tile> tileEntry : tileByComponentCache.entrySet()) { 171 final String packageName = tileEntry.getKey().first; 172 List<Tile> tiles = packageToTileMap.get(packageName); 173 if (tiles == null) { 174 tiles = new ArrayList<>(); 175 packageToTileMap.put(packageName, tiles); 176 } 177 tiles.add(tileEntry.getValue()); 178 } 179 180 for (Entry<String, List<Tile>> entry : packageToTileMap.entrySet()) { 181 final List<Tile> tiles = entry.getValue(); 182 // Loop map, find if all tiles from same package uses old key only. 183 boolean useNewKey = false; 184 boolean useOldKey = false; 185 for (Tile tile : tiles) { 186 if (CategoryKey.KEY_COMPAT_MAP.containsKey(tile.getCategory())) { 187 useOldKey = true; 188 } else { 189 useNewKey = true; 190 break; 191 } 192 } 193 // Uses only old key, map them to new keys one by one. 194 if (useOldKey && !useNewKey) { 195 for (Tile tile : tiles) { 196 final String newCategoryKey = 197 CategoryKey.KEY_COMPAT_MAP.get(tile.getCategory()); 198 tile.setCategory(newCategoryKey); 199 // move tile to new category. 200 DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey); 201 if (newCategory == null) { 202 newCategory = new DashboardCategory(newCategoryKey); 203 categoryByKeyMap.put(newCategoryKey, newCategory); 204 } 205 newCategory.addTile(tile); 206 } 207 } 208 } 209 } 210 211 /** 212 * Sort the tiles injected from all apps such that if they have the same priority value, 213 * they wil lbe sorted by package name. 214 * <p/> 215 * A list of tiles are considered sorted when their priority value decreases in a linear 216 * scan. 217 */ 218 @VisibleForTesting sortCategories(Context context, Map<String, DashboardCategory> categoryByKeyMap)219 synchronized void sortCategories(Context context, 220 Map<String, DashboardCategory> categoryByKeyMap) { 221 for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { 222 categoryEntry.getValue().sortTiles(context.getPackageName()); 223 } 224 } 225 226 /** 227 * Filter out duplicate tiles from category. Duplicate tiles are the ones pointing to the 228 * same intent for ActivityTile, and also the ones having the same description for ProviderTile. 229 */ 230 @VisibleForTesting filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap)231 synchronized void filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap) { 232 for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { 233 final DashboardCategory category = categoryEntry.getValue(); 234 final int count = category.getTilesCount(); 235 final Set<String> descriptions = new ArraySet<>(); 236 final Set<ComponentName> components = new ArraySet<>(); 237 for (int i = count - 1; i >= 0; i--) { 238 final Tile tile = category.getTile(i); 239 if (tile instanceof ProviderTile) { 240 final String desc = tile.getDescription(); 241 if (descriptions.contains(desc)) { 242 category.removeTile(i); 243 } else { 244 descriptions.add(desc); 245 } 246 } else { 247 final ComponentName tileComponent = tile.getIntent().getComponent(); 248 if (components.contains(tileComponent)) { 249 category.removeTile(i); 250 } else { 251 components.add(tileComponent); 252 } 253 } 254 } 255 } 256 } 257 } 258