CategoryManager.java revision b8d2cd41e9472d6f860f35ee9a073b89ba9507f0
1f46c533931224296b11d98798344c049f88db9a1mukesh agrawal/**
2f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * Copyright (C) 2016 The Android Open Source Project
3f46c533931224296b11d98798344c049f88db9a1mukesh agrawal *
4f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * Licensed under the Apache License, Version 2.0 (the "License");
5f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * you may not use this file except in compliance with the License.
6f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * You may obtain a copy of the License at
7f46c533931224296b11d98798344c049f88db9a1mukesh agrawal *
8f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * http://www.apache.org/licenses/LICENSE-2.0
9f46c533931224296b11d98798344c049f88db9a1mukesh agrawal *
10f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * Unless required by applicable law or agreed to in writing, software
11f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * distributed under the License is distributed on an "AS IS" BASIS,
12f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * See the License for the specific language governing permissions and
14f46c533931224296b11d98798344c049f88db9a1mukesh agrawal * limitations under the License.
15f46c533931224296b11d98798344c049f88db9a1mukesh agrawal */
16f46c533931224296b11d98798344c049f88db9a1mukesh agrawalpackage com.android.settingslib.drawer;
17f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
18f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.content.ComponentName;
19f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.content.Context;
20f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.support.annotation.VisibleForTesting;
21f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.util.ArrayMap;
22f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.util.ArraySet;
23f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.util.Log;
24f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport android.util.Pair;
25f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
26f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport com.android.settingslib.applications.InterestingConfigChanges;
27f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
28f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.ArrayList;
29f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.HashMap;
30f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.List;
31f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.Map;
32f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.Map.Entry;
33f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport java.util.Set;
34f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
35f46c533931224296b11d98798344c049f88db9a1mukesh agrawalimport static java.lang.String.CASE_INSENSITIVE_ORDER;
36f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
37f46c533931224296b11d98798344c049f88db9a1mukesh agrawalpublic class CategoryManager {
38f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
39f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    private static final String TAG = "CategoryManager";
40f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
41f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    private static CategoryManager sInstance;
42f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    private final InterestingConfigChanges mInterestingConfigChanges;
43f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
44f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    // Tile cache (key: <packageName, activityName>, value: tile)
45bd9be6d8cab19c78a6bf14b62c4bbb6ef8d13769mukesh agrawal    private final Map<Pair<String, String>, Tile> mTileByComponentCache;
46bd9be6d8cab19c78a6bf14b62c4bbb6ef8d13769mukesh agrawal
47bd9be6d8cab19c78a6bf14b62c4bbb6ef8d13769mukesh agrawal    // Tile cache (key: category key, value: category)
48bd9be6d8cab19c78a6bf14b62c4bbb6ef8d13769mukesh agrawal    private final Map<String, DashboardCategory> mCategoryByKeyMap;
49bd9be6d8cab19c78a6bf14b62c4bbb6ef8d13769mukesh agrawal
50f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    private List<DashboardCategory> mCategories;
51f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    private String mExtraAction;
52f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
53f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    public static CategoryManager get(Context context) {
54f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        return get(context, null);
55f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    }
56f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal
57f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    public static CategoryManager get(Context context, String action) {
58f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        if (sInstance == null) {
59f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal            sInstance = new CategoryManager(context, action);
60f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        }
61f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        return sInstance;
62f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    }
63f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal
64f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    CategoryManager(Context context, String action) {
65f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        mTileByComponentCache = new ArrayMap<>();
66f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        mCategoryByKeyMap = new ArrayMap<>();
67f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        mInterestingConfigChanges = new InterestingConfigChanges();
68f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        mInterestingConfigChanges.applyNewConfig(context.getResources());
69f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        mExtraAction = action;
70f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    }
71f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal
72f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
73f5d90be206db98bbd5894afc8d757dd32360b2d9mukesh agrawal        return getTilesByCategory(context, categoryKey, TileUtils.SETTING_PKG);
74f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    }
75f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
76f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey,
77f46c533931224296b11d98798344c049f88db9a1mukesh agrawal            String settingPkg) {
78f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        tryInitCategories(context, settingPkg);
79f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
80f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        return mCategoryByKeyMap.get(categoryKey);
81f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    }
82f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
83f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    public synchronized List<DashboardCategory> getCategories(Context context) {
84f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        return getCategories(context, TileUtils.SETTING_PKG);
85f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    }
86f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
87f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    public synchronized List<DashboardCategory> getCategories(Context context, String settingPkg) {
88f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        tryInitCategories(context, settingPkg);
89f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        return mCategories;
90f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    }
91f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
92f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    public synchronized void reloadAllCategories(Context context, String settingPkg) {
93f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig(
94f46c533931224296b11d98798344c049f88db9a1mukesh agrawal                context.getResources());
95f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        mCategories = null;
96f46c533931224296b11d98798344c049f88db9a1mukesh agrawal        tryInitCategories(context, forceClearCache, settingPkg);
97f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    }
98f46c533931224296b11d98798344c049f88db9a1mukesh agrawal
99f46c533931224296b11d98798344c049f88db9a1mukesh agrawal    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