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