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