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