• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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