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