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