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