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