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