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