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