Home | History | Annotate | Download | only in drawer
      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.settingslib.drawer;
     17 
     18 import android.content.ComponentName;
     19 import android.content.Context;
     20 import android.support.annotation.VisibleForTesting;
     21 import android.text.TextUtils;
     22 import android.util.ArrayMap;
     23 import android.util.ArraySet;
     24 import android.util.Log;
     25 import android.util.Pair;
     26 
     27 import com.android.settingslib.applications.InterestingConfigChanges;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Collections;
     31 import java.util.HashMap;
     32 import java.util.List;
     33 import java.util.Map;
     34 import java.util.Map.Entry;
     35 import java.util.Set;
     36 
     37 import static java.lang.String.CASE_INSENSITIVE_ORDER;
     38 
     39 public 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