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