• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package com.android.settings.core;
18 
19 import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
20 import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
21 
22 import android.annotation.Nullable;
23 import android.content.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.os.AsyncTask;
29 import android.text.TextUtils;
30 import android.util.ArraySet;
31 import android.util.Log;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.lifecycle.LifecycleObserver;
35 import androidx.lifecycle.OnLifecycleEvent;
36 
37 import com.android.settings.dashboard.CategoryManager;
38 import com.android.settingslib.drawer.Tile;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
44 
45 /**
46  * A mixin that handles live categories for Injection
47  */
48 public class CategoryMixin implements LifecycleObserver {
49 
50     private static final String TAG = "CategoryMixin";
51     private static final String DATA_SCHEME_PKG = "package";
52 
53     // Serves as a temporary list of tiles to ignore until we heard back from the PM that they
54     // are disabled.
55     private static final ArraySet<ComponentName> sTileDenylist = new ArraySet<>();
56 
57     private final Context mContext;
58     private final PackageReceiver mPackageReceiver = new PackageReceiver();
59     private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
60     private int mCategoriesUpdateTaskCount;
61 
CategoryMixin(Context context)62     public CategoryMixin(Context context) {
63         mContext = context;
64     }
65 
66     /**
67      * Resume Lifecycle event
68      */
69     @OnLifecycleEvent(ON_RESUME)
onResume()70     public void onResume() {
71         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
72         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
73         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
74         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
75         filter.addDataScheme(DATA_SCHEME_PKG);
76         mContext.registerReceiver(mPackageReceiver, filter);
77 
78         updateCategories();
79     }
80 
81     /**
82      * Pause Lifecycle event
83      */
84     @OnLifecycleEvent(ON_PAUSE)
onPause()85     public void onPause() {
86         mContext.unregisterReceiver(mPackageReceiver);
87     }
88 
89     /**
90      * Add a category listener
91      */
addCategoryListener(CategoryListener listener)92     public void addCategoryListener(CategoryListener listener) {
93         mCategoryListeners.add(listener);
94     }
95 
96     /**
97      * Remove a category listener
98      */
removeCategoryListener(CategoryListener listener)99     public void removeCategoryListener(CategoryListener listener) {
100         mCategoryListeners.remove(listener);
101     }
102 
103     /**
104      * Updates dashboard categories.
105      */
updateCategories()106     public void updateCategories() {
107         updateCategories(false /* fromBroadcast */);
108     }
109 
addToDenylist(ComponentName component)110     void addToDenylist(ComponentName component) {
111         sTileDenylist.add(component);
112     }
113 
removeFromDenylist(ComponentName component)114     void removeFromDenylist(ComponentName component) {
115         sTileDenylist.remove(component);
116     }
117 
118     @VisibleForTesting
onCategoriesChanged(Set<String> categories)119     void onCategoriesChanged(Set<String> categories) {
120         mCategoryListeners.forEach(listener -> listener.onCategoriesChanged(categories));
121     }
122 
updateCategories(boolean fromBroadcast)123     private void updateCategories(boolean fromBroadcast) {
124         // Only allow at most 2 tasks existing at the same time since when the first one is
125         // executing, there may be new data from the second update request.
126         // Ignore the third update request because the second task is still waiting for the first
127         // task to complete in a serial thread, which will get the latest data.
128         if (mCategoriesUpdateTaskCount < 2) {
129             new CategoriesUpdateTask().execute(fromBroadcast);
130         }
131     }
132 
133     /**
134      * A handler implementing a {@link CategoryMixin}
135      */
136     public interface CategoryHandler {
137         /** returns a {@link CategoryMixin} */
getCategoryMixin()138         CategoryMixin getCategoryMixin();
139     }
140 
141     /**
142      *  A listener receiving category change events.
143      */
144     public interface CategoryListener {
145         /**
146          * @param categories the changed categories that have to be refreshed, or null to force
147          *                   refreshing all.
148          */
onCategoriesChanged(@ullable Set<String> categories)149         void onCategoriesChanged(@Nullable Set<String> categories);
150     }
151 
152     private class CategoriesUpdateTask extends AsyncTask<Boolean, Void, Set<String>> {
153 
154         private final CategoryManager mCategoryManager;
155         private Map<ComponentName, Tile> mPreviousTileMap;
156 
CategoriesUpdateTask()157         CategoriesUpdateTask() {
158             mCategoriesUpdateTaskCount++;
159             mCategoryManager = CategoryManager.get(mContext);
160         }
161 
162         @Override
doInBackground(Boolean... params)163         protected Set<String> doInBackground(Boolean... params) {
164             mPreviousTileMap = mCategoryManager.getTileByComponentMap();
165             mCategoryManager.reloadAllCategories(mContext);
166             mCategoryManager.updateCategoryFromDenylist(sTileDenylist);
167             return getChangedCategories(params[0]);
168         }
169 
170         @Override
onPostExecute(Set<String> categories)171         protected void onPostExecute(Set<String> categories) {
172             if (categories == null || !categories.isEmpty()) {
173                 onCategoriesChanged(categories);
174             }
175             mCategoriesUpdateTaskCount--;
176         }
177 
178         // Return the changed categories that have to be refreshed, or null to force refreshing all.
getChangedCategories(boolean fromBroadcast)179         private Set<String> getChangedCategories(boolean fromBroadcast) {
180             if (!fromBroadcast) {
181                 // Always refresh for non-broadcast case.
182                 return null;
183             }
184 
185             final Set<String> changedCategories = new ArraySet<>();
186             final Map<ComponentName, Tile> currentTileMap =
187                     mCategoryManager.getTileByComponentMap();
188             currentTileMap.forEach((component, currentTile) -> {
189                 final Tile previousTile = mPreviousTileMap.get(component);
190                 // Check if the tile is newly added.
191                 if (previousTile == null) {
192                     Log.i(TAG, "Tile added: " + component.flattenToShortString());
193                     changedCategories.add(currentTile.getCategory());
194                     return;
195                 }
196 
197                 // Check if the title or summary has changed.
198                 if (!TextUtils.equals(currentTile.getTitle(mContext),
199                         previousTile.getTitle(mContext))
200                         || !TextUtils.equals(currentTile.getSummary(mContext),
201                         previousTile.getSummary(mContext))) {
202                     Log.i(TAG, "Tile changed: " + component.flattenToShortString());
203                     changedCategories.add(currentTile.getCategory());
204                 }
205             });
206 
207             // Check if any previous tile is removed.
208             final Set<ComponentName> removal = new ArraySet(mPreviousTileMap.keySet());
209             removal.removeAll(currentTileMap.keySet());
210             removal.forEach(component -> {
211                 Log.i(TAG, "Tile removed: " + component.flattenToShortString());
212                 changedCategories.add(mPreviousTileMap.get(component).getCategory());
213             });
214 
215             return changedCategories;
216         }
217     }
218 
219     private class PackageReceiver extends BroadcastReceiver {
220         @Override
onReceive(Context context, Intent intent)221         public void onReceive(Context context, Intent intent) {
222             updateCategories(true /* fromBroadcast */);
223         }
224     }
225 }
226