blob: b55de4b74469b2d10c5dc97d92bddf9e3df604da [file] [log] [blame]
Fan Zhangbb6d2602016-10-04 13:21:06 -07001/*
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 */
16package com.android.settings.dashboard;
17
18import android.app.Activity;
Fan Zhangff47b302018-11-09 14:52:42 -080019import android.app.settings.SettingsEnums;
Jason Chiubac59872019-10-24 17:30:34 +080020import android.content.ContentResolver;
Fan Zhangbb6d2602016-10-04 13:21:06 -070021import android.content.Context;
jasonwshsud3b3ed52023-04-05 22:14:49 +080022import android.content.Intent;
Fan Zhang36924652016-10-07 08:38:48 -070023import android.os.Bundle;
jasonwshsud3b3ed52023-04-05 22:14:49 +080024import android.preference.PreferenceManager.OnActivityResultListener;
Fan Zhangbb6d2602016-10-04 13:21:06 -070025import android.text.TextUtils;
26import android.util.ArrayMap;
27import android.util.Log;
Chaohui Wangeb4fea12023-06-13 17:08:33 +080028import android.view.View;
Fan Zhangbb6d2602016-10-04 13:21:06 -070029
Yanting Yang4e56cb22019-04-18 21:37:31 +080030import androidx.annotation.CallSuper;
Chaohui Wangeb4fea12023-06-13 17:08:33 +080031import androidx.annotation.NonNull;
jasonwshsud3b3ed52023-04-05 22:14:49 +080032import androidx.annotation.Nullable;
Fan Zhang1c61a582018-07-26 11:26:11 -070033import androidx.annotation.VisibleForTesting;
Chaohui Wang70470582022-03-29 14:37:53 +080034import androidx.lifecycle.LifecycleObserver;
Chaohui Wangeb4fea12023-06-13 17:08:33 +080035import androidx.lifecycle.LifecycleOwner;
Fan Zhang1c61a582018-07-26 11:26:11 -070036import androidx.preference.Preference;
Peter Zhangbaeca6f2023-05-08 16:59:01 +020037import androidx.preference.PreferenceCategory;
Doris Ling8741c042018-08-27 14:11:55 -070038import androidx.preference.PreferenceGroup;
Fan Zhang1c61a582018-07-26 11:26:11 -070039import androidx.preference.PreferenceManager;
40import androidx.preference.PreferenceScreen;
Chaohui Wangcfdb3b12023-10-24 11:38:26 +080041import androidx.preference.SwitchPreferenceCompat;
Fan Zhang1c61a582018-07-26 11:26:11 -070042
Yanting Yang4e56cb22019-04-18 21:37:31 +080043import com.android.settings.R;
Fan Zhangbb6d2602016-10-04 13:21:06 -070044import com.android.settings.SettingsPreferenceFragment;
Fan Zhang917f1012018-02-21 15:22:25 -080045import com.android.settings.core.BasePreferenceController;
Jason Chiuc713c3e2021-06-17 17:48:02 +080046import com.android.settings.core.CategoryMixin.CategoryHandler;
47import com.android.settings.core.CategoryMixin.CategoryListener;
Fan Zhang917f1012018-02-21 15:22:25 -080048import com.android.settings.core.PreferenceControllerListHelper;
Fan Zhangbb6d2602016-10-04 13:21:06 -070049import com.android.settings.overlay.FeatureFactory;
Wilson Wu9edf7972021-08-27 10:19:56 +080050import com.android.settingslib.PrimarySwitchPreference;
Tony Mantler1d583e12017-06-13 13:09:25 -070051import com.android.settingslib.core.AbstractPreferenceController;
Fan Zhang917f1012018-02-21 15:22:25 -080052import com.android.settingslib.core.lifecycle.Lifecycle;
Fan Zhangbb6d2602016-10-04 13:21:06 -070053import com.android.settingslib.drawer.DashboardCategory;
Fan Zhangbb6d2602016-10-04 13:21:06 -070054import com.android.settingslib.drawer.Tile;
Jacky Wang20558e22024-10-12 16:09:15 +080055import com.android.settingslib.preference.PreferenceScreenCreator;
Raff Tsai6db277e2019-10-02 16:43:23 +080056import com.android.settingslib.search.Indexable;
Fan Zhangbb6d2602016-10-04 13:21:06 -070057
Fan Zhanga1a84e62016-10-19 14:15:34 -070058import java.util.ArrayList;
Yanting Yang4e56cb22019-04-18 21:37:31 +080059import java.util.Arrays;
Fan Zhangbb6d2602016-10-04 13:21:06 -070060import java.util.Collection;
Christian Göllnerff9065a2022-02-17 11:32:54 +000061import java.util.Collections;
Peter Zhangbaeca6f2023-05-08 16:59:01 +020062import java.util.Comparator;
Jacky Wang20558e22024-10-12 16:09:15 +080063import java.util.Iterator;
Fan Zhangbb6d2602016-10-04 13:21:06 -070064import java.util.List;
65import java.util.Map;
Jason Chiubac59872019-10-24 17:30:34 +080066import java.util.Objects;
Jason Chiu20df25e2020-09-30 14:08:33 +080067import java.util.Set;
Jason Chiuc9615612022-05-06 11:14:54 +080068import java.util.concurrent.CountDownLatch;
69import java.util.concurrent.TimeUnit;
Fan Zhangbb6d2602016-10-04 13:21:06 -070070
71/**
72 * Base fragment for dashboard style UI containing a list of static and dynamic setting items.
73 */
74public abstract class DashboardFragment extends SettingsPreferenceFragment
Jason Chiuc713c3e2021-06-17 17:48:02 +080075 implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
jackqdyulei22904a02018-12-27 14:40:09 -080076 BasePreferenceController.UiBlockListener {
Raff Tsaif71db732019-12-02 10:55:29 +080077 public static final String CATEGORY = "category";
Fan Zhangdb1112a2016-10-18 12:58:31 -070078 private static final String TAG = "DashboardFragment";
Jason Chiuc9615612022-05-06 11:14:54 +080079 private static final long TIMEOUT_MILLIS = 50L;
Fan Zhangbb6d2602016-10-04 13:21:06 -070080
Jason Chiubac59872019-10-24 17:30:34 +080081 @VisibleForTesting
82 final ArrayMap<String, List<DynamicDataObserver>> mDashboardTilePrefKeys = new ArrayMap<>();
Ben Lin92751772017-12-21 17:32:34 -080083 private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers =
Fan Zhangbb6d2602016-10-04 13:21:06 -070084 new ArrayMap<>();
Jason Chiubac59872019-10-24 17:30:34 +080085 private final List<DynamicDataObserver> mRegisteredObservers = new ArrayList<>();
hughchend49de612019-09-24 10:30:49 +080086 private final List<AbstractPreferenceController> mControllers = new ArrayList<>();
Sunny Shaoe5e82402019-11-27 18:55:16 +080087 @VisibleForTesting
88 UiBlockerController mBlockerController;
Fan Zhangb297abc2018-02-21 09:37:10 -080089 private DashboardFeatureProvider mDashboardFeatureProvider;
Fan Zhang9dc9c612016-11-11 13:23:21 -080090 private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController;
Fan Zhang36924652016-10-07 08:38:48 -070091 private boolean mListeningToCategoryChange;
Yanting Yang4e56cb22019-04-18 21:37:31 +080092 private List<String> mSuppressInjectedTileKeys;
Fan Zhangbb6d2602016-10-04 13:21:06 -070093
94 @Override
95 public void onAttach(Context context) {
96 super.onAttach(context);
Yanting Yang4e56cb22019-04-18 21:37:31 +080097 mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray(
98 R.array.config_suppress_injected_tile_keys));
Chaohui Wang58f0ee32023-07-20 12:21:16 +080099 mDashboardFeatureProvider =
100 FeatureFactory.getFeatureFactory().getDashboardFeatureProvider();
Fan Zhang66b573a2016-10-06 16:33:13 -0700101
Jacky Wang20558e22024-10-12 16:09:15 +0800102 PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
103 if (preferenceScreenCreator == null || !preferenceScreenCreator.hasCompleteHierarchy()) {
Jacky Wangb3e1b522024-09-09 20:58:38 +0800104 // Load preference controllers from code
105 final List<AbstractPreferenceController> controllersFromCode =
106 createPreferenceControllers(context);
107 // Load preference controllers from xml definition
108 final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
109 .getPreferenceControllersFromXml(context, getPreferenceScreenResId());
110 // Filter xml-based controllers in case a similar controller is created from code
111 // already.
112 final List<BasePreferenceController> uniqueControllerFromXml =
113 PreferenceControllerListHelper.filterControllers(
114 controllersFromXml, controllersFromCode);
Fan Zhang917f1012018-02-21 15:22:25 -0800115
Jacky Wangb3e1b522024-09-09 20:58:38 +0800116 // Add unique controllers to list.
117 if (controllersFromCode != null) {
118 mControllers.addAll(controllersFromCode);
Jason Chiu1a1f9312019-10-01 17:35:00 +0800119 }
Jacky Wangb3e1b522024-09-09 20:58:38 +0800120 mControllers.addAll(uniqueControllerFromXml);
121
122 // And wire up with lifecycle.
123 final Lifecycle lifecycle = getSettingsLifecycle();
124 uniqueControllerFromXml.forEach(controller -> {
125 if (controller instanceof LifecycleObserver) {
126 lifecycle.addObserver((LifecycleObserver) controller);
127 }
128 });
129 }
Fan Zhang917f1012018-02-21 15:22:25 -0800130
Jason Chiu819abf72020-04-01 15:40:47 +0800131 // Set metrics category for BasePreferenceController.
132 final int metricCategory = getMetricsCategory();
133 mControllers.forEach(controller -> {
134 if (controller instanceof BasePreferenceController) {
135 ((BasePreferenceController) controller).setMetricsCategory(metricCategory);
136 }
137 });
138
Fan Zhang9dc9c612016-11-11 13:23:21 -0800139 mPlaceholderPreferenceController =
140 new DashboardTilePlaceholderPreferenceController(context);
hughchend49de612019-09-24 10:30:49 +0800141 mControllers.add(mPlaceholderPreferenceController);
142 for (AbstractPreferenceController controller : mControllers) {
Fan Zhang66b573a2016-10-06 16:33:13 -0700143 addPreferenceController(controller);
144 }
jackqdyulei22904a02018-12-27 14:40:09 -0800145 }
146
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800147 @VisibleForTesting
148 void checkUiBlocker(List<AbstractPreferenceController> controllers) {
jackqdyulei22904a02018-12-27 14:40:09 -0800149 final List<String> keys = new ArrayList<>();
Yi-Ling Chuang61022c62022-04-18 15:35:52 +0800150 final List<BasePreferenceController> baseControllers = new ArrayList<>();
Jason Chiu1a1f9312019-10-01 17:35:00 +0800151 controllers.forEach(controller -> {
152 if (controller instanceof BasePreferenceController.UiBlocker
153 && controller.isAvailable()) {
154 ((BasePreferenceController) controller).setUiBlockListener(this);
155 keys.add(controller.getPreferenceKey());
Yi-Ling Chuang61022c62022-04-18 15:35:52 +0800156 baseControllers.add((BasePreferenceController) controller);
Jason Chiu1a1f9312019-10-01 17:35:00 +0800157 }
158 });
jackqdyulei22904a02018-12-27 14:40:09 -0800159
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800160 if (!keys.isEmpty()) {
161 mBlockerController = new UiBlockerController(keys);
Yi-Ling Chuang61022c62022-04-18 15:35:52 +0800162 mBlockerController.start(() -> {
163 updatePreferenceVisibility(mPreferenceControllers);
164 baseControllers.forEach(controller -> controller.setUiBlockerFinished(true));
165 });
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800166 }
Fan Zhangbb6d2602016-10-04 13:21:06 -0700167 }
168
169 @Override
Fan Zhanga1a84e62016-10-19 14:15:34 -0700170 public void onCreate(Bundle icicle) {
171 super.onCreate(icicle);
172 // Set ComparisonCallback so we get better animation when list changes.
173 getPreferenceManager().setPreferenceComparisonCallback(
174 new PreferenceManager.SimplePreferenceComparisonCallback());
jeffreyhuangbf234af2017-11-14 15:09:12 -0800175 if (icicle != null) {
176 // Upon rotation configuration change we need to update preference states before any
177 // editing dialog is recreated (that would happen before onResume is called).
178 updatePreferenceStates();
179 }
Fan Zhanga1a84e62016-10-19 14:15:34 -0700180 }
181
182 @Override
Chaohui Wangeb4fea12023-06-13 17:08:33 +0800183 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
184 super.onViewCreated(view, savedInstanceState);
185 LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
186 for (AbstractPreferenceController controller : mControllers) {
187 controller.onViewCreated(viewLifecycleOwner);
188 }
189 }
190
191 @Override
Jason Chiu20df25e2020-09-30 14:08:33 +0800192 public void onCategoriesChanged(Set<String> categories) {
193 final String categoryKey = getCategoryKey();
194 final DashboardCategory dashboardCategory =
195 mDashboardFeatureProvider.getTilesForCategory(categoryKey);
196 if (dashboardCategory == null) {
Fan Zhang36924652016-10-07 08:38:48 -0700197 return;
198 }
Jason Chiu20df25e2020-09-30 14:08:33 +0800199
200 if (categories == null) {
201 // force refreshing
202 refreshDashboardTiles(getLogTag());
203 } else if (categories.contains(categoryKey)) {
204 Log.i(TAG, "refresh tiles for " + categoryKey);
205 refreshDashboardTiles(getLogTag());
206 }
Fan Zhang36924652016-10-07 08:38:48 -0700207 }
208
209 @Override
210 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
hughchend49de612019-09-24 10:30:49 +0800211 checkUiBlocker(mControllers);
Fan Zhang36924652016-10-07 08:38:48 -0700212 refreshAllPreferences(getLogTag());
Jason Chiub12e3b92020-04-07 16:30:18 +0800213 mControllers.stream()
214 .map(controller -> (Preference) findPreference(controller.getPreferenceKey()))
215 .filter(Objects::nonNull)
216 .forEach(preference -> {
217 // Give all controllers a chance to handle click.
218 preference.getExtras().putInt(CATEGORY, getMetricsCategory());
219 });
Fan Zhang36924652016-10-07 08:38:48 -0700220 }
221
222 @Override
Fan Zhangbb6d2602016-10-04 13:21:06 -0700223 public void onStart() {
224 super.onStart();
Fan Zhangfabbfb42016-10-07 12:41:43 -0700225 final DashboardCategory category =
226 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
Fan Zhang36924652016-10-07 08:38:48 -0700227 if (category == null) {
228 return;
229 }
Fan Zhangbb6d2602016-10-04 13:21:06 -0700230 final Activity activity = getActivity();
Jason Chiuc713c3e2021-06-17 17:48:02 +0800231 if (activity instanceof CategoryHandler) {
Fan Zhang36924652016-10-07 08:38:48 -0700232 mListeningToCategoryChange = true;
Jason Chiuc713c3e2021-06-17 17:48:02 +0800233 ((CategoryHandler) activity).getCategoryMixin().addCategoryListener(this);
Fan Zhangbb6d2602016-10-04 13:21:06 -0700234 }
Jason Chiubac59872019-10-24 17:30:34 +0800235 final ContentResolver resolver = getContentResolver();
236 mDashboardTilePrefKeys.values().stream()
237 .filter(Objects::nonNull)
238 .flatMap(List::stream)
239 .forEach(observer -> {
240 if (!mRegisteredObservers.contains(observer)) {
241 registerDynamicDataObserver(resolver, observer);
242 }
243 });
Fan Zhangbb6d2602016-10-04 13:21:06 -0700244 }
245
246 @Override
Fan Zhang66b573a2016-10-06 16:33:13 -0700247 public void onResume() {
248 super.onResume();
249 updatePreferenceStates();
250 }
251
252 @Override
Fan Zhangbb6d2602016-10-04 13:21:06 -0700253 public boolean onPreferenceTreeClick(Preference preference) {
Jacky Wang1c486032024-09-25 11:58:10 +0800254 if (isCatalystEnabled()) {
255 Intent intent = preference.getIntent();
256 if (intent != null && preference.getContext().getPackageManager().queryIntentActivities(
257 intent, 0).isEmpty()) {
258 Log.w(TAG, "No activity to start for " + intent);
259 return true;
260 }
261 }
262
Jason Chiu1281e592019-12-12 17:31:28 +0800263 final Collection<List<AbstractPreferenceController>> controllers =
Ben Lin92751772017-12-21 17:32:34 -0800264 mPreferenceControllers.values();
Ben Lin92751772017-12-21 17:32:34 -0800265 for (List<AbstractPreferenceController> controllerList : controllers) {
266 for (AbstractPreferenceController controller : controllerList) {
267 if (controller.handlePreferenceTreeClick(preference)) {
Jason Chiu1281e592019-12-12 17:31:28 +0800268 // log here since calling super.onPreferenceTreeClick will be skipped
269 writePreferenceClickMetric(preference);
Ben Lin92751772017-12-21 17:32:34 -0800270 return true;
271 }
Fan Zhangbb6d2602016-10-04 13:21:06 -0700272 }
273 }
274 return super.onPreferenceTreeClick(preference);
275 }
276
277 @Override
278 public void onStop() {
279 super.onStop();
Jason Chiubac59872019-10-24 17:30:34 +0800280 unregisterDynamicDataObservers(new ArrayList<>(mRegisteredObservers));
Fan Zhang36924652016-10-07 08:38:48 -0700281 if (mListeningToCategoryChange) {
282 final Activity activity = getActivity();
Jason Chiuc713c3e2021-06-17 17:48:02 +0800283 if (activity instanceof CategoryHandler) {
284 ((CategoryHandler) activity).getCategoryMixin().removeCategoryListener(this);
Fan Zhang36924652016-10-07 08:38:48 -0700285 }
286 mListeningToCategoryChange = false;
Fan Zhangbb6d2602016-10-04 13:21:06 -0700287 }
288 }
289
Fan Zhang9679dba2017-10-31 14:27:18 -0700290 @Override
291 protected abstract int getPreferenceScreenResId();
292
Doris Ling8741c042018-08-27 14:11:55 -0700293 @Override
294 public void onExpandButtonClick() {
Fan Zhangff47b302018-11-09 14:52:42 -0800295 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
Fan Zhang31b21002019-01-16 13:49:47 -0800296 SettingsEnums.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND,
Fan Zhangff47b302018-11-09 14:52:42 -0800297 getMetricsCategory(), null, 0);
Doris Ling8741c042018-08-27 14:11:55 -0700298 }
299
jasonwshsud3b3ed52023-04-05 22:14:49 +0800300 @Override
301 public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
302 for (List<AbstractPreferenceController> controllerList : mPreferenceControllers.values()) {
303 for (AbstractPreferenceController controller : controllerList) {
304 if (controller instanceof OnActivityResultListener) {
305 ((OnActivityResultListener) controller).onActivityResult(
306 requestCode, resultCode, data);
307 }
308 }
309 }
310 super.onActivityResult(requestCode, resultCode, data);
311 }
312
Fan Zhang7d5a9ee2018-08-09 17:32:37 -0700313 protected boolean shouldForceRoundedIcon() {
314 return false;
315 }
316
Fan Zhangf7843ad2018-02-22 13:51:41 -0800317 protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
Ben Lin92751772017-12-21 17:32:34 -0800318 List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);
319 if (controllerList != null) {
320 if (controllerList.size() > 1) {
321 Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName()
322 + " found, returning first one.");
323 }
324 return (T) controllerList.get(0);
325 }
326
327 return null;
Fan Zhangbb6d2602016-10-04 13:21:06 -0700328 }
329
tomhsu253cb872024-12-06 10:37:34 +0000330 /** Returns grouped controllers of input type T. */
331 protected <T extends AbstractPreferenceController> List<AbstractPreferenceController> useGroup(
332 Class<T> clazz) {
333 return mPreferenceControllers.values().stream().flatMap(Collection::stream).filter(
334 controller -> clazz.isInstance(controller)).toList();
335 }
336
Christian Göllnerff9065a2022-02-17 11:32:54 +0000337 /** Returns all controllers of type T. */
338 protected <T extends AbstractPreferenceController> List<T> useAll(Class<T> clazz) {
339 return (List<T>) mPreferenceControllers.getOrDefault(clazz, Collections.emptyList());
340 }
341
Tony Mantler1d583e12017-06-13 13:09:25 -0700342 protected void addPreferenceController(AbstractPreferenceController controller) {
Ben Lin92751772017-12-21 17:32:34 -0800343 if (mPreferenceControllers.get(controller.getClass()) == null) {
344 mPreferenceControllers.put(controller.getClass(), new ArrayList<>());
345 }
346 mPreferenceControllers.get(controller.getClass()).add(controller);
Fan Zhangbb6d2602016-10-04 13:21:06 -0700347 }
348
Fan Zhang36924652016-10-07 08:38:48 -0700349 /**
Fan Zhangfabbfb42016-10-07 12:41:43 -0700350 * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
Fan Zhang36924652016-10-07 08:38:48 -0700351 */
Fan Zhang26a1def2017-07-26 10:58:32 -0700352 @VisibleForTesting
Fan Zhang7e6df832017-01-24 14:02:17 -0800353 public String getCategoryKey() {
354 return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
355 }
Fan Zhang36924652016-10-07 08:38:48 -0700356
357 /**
Fan Zhang66b573a2016-10-06 16:33:13 -0700358 * Get the tag string for logging.
359 */
360 protected abstract String getLogTag();
361
362 /**
Tony Mantler1d583e12017-06-13 13:09:25 -0700363 * Get a list of {@link AbstractPreferenceController} for this fragment.
Fan Zhang66b573a2016-10-06 16:33:13 -0700364 */
Fan Zhangf7843ad2018-02-22 13:51:41 -0800365 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
Fan Zhang917f1012018-02-21 15:22:25 -0800366 return null;
367 }
Fan Zhang66b573a2016-10-06 16:33:13 -0700368
369 /**
Doris Ling20d4b042016-11-22 16:37:06 -0800370 * Returns true if this tile should be displayed
371 */
Yanting Yang4e56cb22019-04-18 21:37:31 +0800372 @CallSuper
Doris Ling20d4b042016-11-22 16:37:06 -0800373 protected boolean displayTile(Tile tile) {
Yanting Yang4e56cb22019-04-18 21:37:31 +0800374 if (mSuppressInjectedTileKeys != null && tile.hasKey()) {
375 // For suppressing injected tiles for OEMs.
376 return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext()));
377 }
Doris Ling20d4b042016-11-22 16:37:06 -0800378 return true;
379 }
380
381 /**
Fan Zhang36924652016-10-07 08:38:48 -0700382 * Displays resource based tiles.
383 */
Fan Zhang66b573a2016-10-06 16:33:13 -0700384 private void displayResourceTiles() {
Jacky Wangb3e1b522024-09-09 20:58:38 +0800385 PreferenceScreen screen;
Jacky Wang20558e22024-10-12 16:09:15 +0800386 PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
387 if (preferenceScreenCreator != null) {
Jacky Wangb3e1b522024-09-09 20:58:38 +0800388 screen = createPreferenceScreen();
Jacky Wang20558e22024-10-12 16:09:15 +0800389 if (!preferenceScreenCreator.hasCompleteHierarchy()) {
390 removeControllersForHybridMode();
391 }
Jacky Wangb3e1b522024-09-09 20:58:38 +0800392 setPreferenceScreen(screen);
Jacky Wangb3e1b522024-09-09 20:58:38 +0800393 } else {
Jacky Wang4cadb752025-01-22 11:21:54 +0800394 final int resId = getPreferenceScreenResId();
395 if (resId <= 0) {
396 return;
397 }
Jacky Wangb3e1b522024-09-09 20:58:38 +0800398 addPreferencesFromResource(resId);
399 screen = getPreferenceScreen();
400 }
Doris Ling8741c042018-08-27 14:11:55 -0700401 screen.setOnExpandButtonClickListener(this);
Bonian Chen048d9fe2020-04-20 12:33:49 +0800402 displayResourceTilesToScreen(screen);
403 }
404
Jacky Wang20558e22024-10-12 16:09:15 +0800405 /**
406 * Removes preference controllers that have been migrated to catalyst.
407 *
408 * In hybrid mode, preference screen is inflated from XML resource, while preference metadata
409 * in the preference hierarchy are used to update preference widget UI. To avoid conflict,
410 * remove the preference controllers.
411 */
412 private void removeControllersForHybridMode() {
413 Set<String> keys = getPreferenceKeysInHierarchy();
414 Iterator<AbstractPreferenceController> iterator = mControllers.iterator();
Jacky Wangb6132572024-11-20 19:08:26 +0800415 Lifecycle lifecycle = getSettingsLifecycle();
Jacky Wang20558e22024-10-12 16:09:15 +0800416 while (iterator.hasNext()) {
417 AbstractPreferenceController controller = iterator.next();
418 String key = controller.getPreferenceKey();
419 if (keys.contains(key)) {
420 Log.i(TAG, "Remove preference controller for " + key);
421 iterator.remove();
422 List<AbstractPreferenceController> controllers = mPreferenceControllers.get(
423 controller.getClass());
424 if (controllers != null) {
425 controllers.remove(controller);
426 }
Jacky Wangb6132572024-11-20 19:08:26 +0800427 if (controller instanceof LifecycleObserver) {
428 lifecycle.removeObserver((LifecycleObserver) controller);
429 }
Jacky Wang20558e22024-10-12 16:09:15 +0800430 }
431 }
432 }
433
Bonian Chen048d9fe2020-04-20 12:33:49 +0800434 /**
435 * Perform {@link AbstractPreferenceController#displayPreference(PreferenceScreen)}
436 * on all {@link AbstractPreferenceController}s.
437 */
438 protected void displayResourceTilesToScreen(PreferenceScreen screen) {
Ben Lin92751772017-12-21 17:32:34 -0800439 mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach(
440 controller -> controller.displayPreference(screen));
Fan Zhang66b573a2016-10-06 16:33:13 -0700441 }
Fan Zhang36924652016-10-07 08:38:48 -0700442
443 /**
Bonian Chen1b9bda32020-02-04 10:27:50 +0800444 * Get current PreferenceController(s)
445 */
446 protected Collection<List<AbstractPreferenceController>> getPreferenceControllers() {
447 return mPreferenceControllers.values();
448 }
449
450 /**
Fan Zhang8b5bca52016-10-19 12:00:32 -0700451 * Update state of each preference managed by PreferenceController.
Fan Zhang36924652016-10-07 08:38:48 -0700452 */
Fan Zhangbeddff82016-11-18 11:10:05 -0800453 protected void updatePreferenceStates() {
Fan Zhang8b5bca52016-10-19 12:00:32 -0700454 final PreferenceScreen screen = getPreferenceScreen();
Ben Lin92751772017-12-21 17:32:34 -0800455 Collection<List<AbstractPreferenceController>> controllerLists =
456 mPreferenceControllers.values();
457 for (List<AbstractPreferenceController> controllerList : controllerLists) {
458 for (AbstractPreferenceController controller : controllerList) {
459 if (!controller.isAvailable()) {
460 continue;
461 }
Jason Chiua42a93b2018-12-18 18:27:50 +0800462
Ben Lin92751772017-12-21 17:32:34 -0800463 final String key = controller.getPreferenceKey();
Jason Chiua42a93b2018-12-18 18:27:50 +0800464 if (TextUtils.isEmpty(key)) {
465 Log.d(TAG, String.format("Preference key is %s in Controller %s",
466 key, controller.getClass().getSimpleName()));
467 continue;
468 }
Fan Zhang8b5bca52016-10-19 12:00:32 -0700469
Ben Lin92751772017-12-21 17:32:34 -0800470 final Preference preference = screen.findPreference(key);
471 if (preference == null) {
472 Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s",
473 key, controller.getClass().getSimpleName()));
474 continue;
475 }
476 controller.updateState(preference);
Fan Zhang8b5bca52016-10-19 12:00:32 -0700477 }
Fan Zhang8b5bca52016-10-19 12:00:32 -0700478 }
479 }
480
Fan Zhang8b5bca52016-10-19 12:00:32 -0700481 /**
482 * Refresh all preference items, including both static prefs from xml, and dynamic items from
483 * DashboardCategory.
484 */
Jason Chiubac59872019-10-24 17:30:34 +0800485 private void refreshAllPreferences(final String tag) {
jackqdyulei22904a02018-12-27 14:40:09 -0800486 final PreferenceScreen screen = getPreferenceScreen();
Fan Zhang8b5bca52016-10-19 12:00:32 -0700487 // First remove old preferences.
jackqdyulei22904a02018-12-27 14:40:09 -0800488 if (screen != null) {
Fan Zhang8b5bca52016-10-19 12:00:32 -0700489 // Intentionally do not cache PreferenceScreen because it will be recreated later.
jackqdyulei22904a02018-12-27 14:40:09 -0800490 screen.removeAll();
Fan Zhang8b5bca52016-10-19 12:00:32 -0700491 }
492
493 // Add resource based tiles.
494 displayResourceTiles();
495
Jason Chiubac59872019-10-24 17:30:34 +0800496 refreshDashboardTiles(tag);
Fan Zhang293883b2018-10-26 14:53:12 -0700497
498 final Activity activity = getActivity();
499 if (activity != null) {
Jason Chiubac59872019-10-24 17:30:34 +0800500 Log.d(tag, "All preferences added, reporting fully drawn");
Fan Zhang293883b2018-10-26 14:53:12 -0700501 activity.reportFullyDrawn();
502 }
jackqdyulei22904a02018-12-27 14:40:09 -0800503
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800504 updatePreferenceVisibility(mPreferenceControllers);
jackqdyulei22904a02018-12-27 14:40:09 -0800505 }
506
Jason Chiuc8975552022-07-06 16:53:59 +0800507 /**
508 * Force update all the preferences in this fragment.
509 */
510 public void forceUpdatePreferences() {
511 final PreferenceScreen screen = getPreferenceScreen();
512 if (screen == null || mPreferenceControllers == null) {
513 return;
514 }
515 for (List<AbstractPreferenceController> controllerList : mPreferenceControllers.values()) {
516 for (AbstractPreferenceController controller : controllerList) {
517 final String key = controller.getPreferenceKey();
518 final Preference preference = findPreference(key);
519 if (preference == null) {
520 continue;
521 }
522 final boolean available = controller.isAvailable();
523 if (available) {
524 controller.updateState(preference);
525 }
526 preference.setVisible(available);
527 }
528 }
529 }
530
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800531 @VisibleForTesting
532 void updatePreferenceVisibility(
533 Map<Class, List<AbstractPreferenceController>> preferenceControllers) {
jackqdyulei22904a02018-12-27 14:40:09 -0800534 final PreferenceScreen screen = getPreferenceScreen();
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800535 if (screen == null || preferenceControllers == null || mBlockerController == null) {
jackqdyulei22904a02018-12-27 14:40:09 -0800536 return;
537 }
538
539 final boolean visible = mBlockerController.isBlockerFinished();
540 for (List<AbstractPreferenceController> controllerList :
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800541 preferenceControllers.values()) {
jackqdyulei22904a02018-12-27 14:40:09 -0800542 for (AbstractPreferenceController controller : controllerList) {
543 final String key = controller.getPreferenceKey();
jackqdyuleia5f1b5c2019-01-14 10:55:39 -0800544 final Preference preference = findPreference(key);
Yi-Ling Chuanga28770e2022-03-08 18:20:15 +0800545 if (preference == null) {
546 continue;
547 }
548 if (controller instanceof BasePreferenceController.UiBlocker) {
549 final boolean prefVisible =
550 ((BasePreferenceController) controller).getSavedPrefVisibility();
551 preference.setVisible(visible && controller.isAvailable() && prefVisible);
552 } else {
jackqdyulei22904a02018-12-27 14:40:09 -0800553 preference.setVisible(visible && controller.isAvailable());
554 }
555 }
556 }
Fan Zhang8b5bca52016-10-19 12:00:32 -0700557 }
558
559 /**
560 * Refresh preference items backed by DashboardCategory.
561 */
Jacky Wangb1d2cd82025-05-13 20:16:30 +0800562 protected void refreshDashboardTiles(final String tag) {
Fan Zhang8b5bca52016-10-19 12:00:32 -0700563 final PreferenceScreen screen = getPreferenceScreen();
Fan Zhanga1a84e62016-10-19 14:15:34 -0700564
Fan Zhangfabbfb42016-10-07 12:41:43 -0700565 final DashboardCategory category =
566 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
Fan Zhang36924652016-10-07 08:38:48 -0700567 if (category == null) {
Jason Chiubac59872019-10-24 17:30:34 +0800568 Log.d(tag, "NO dashboard tiles for " + tag);
Fan Zhang36924652016-10-07 08:38:48 -0700569 return;
570 }
Doris Lingbcb76352017-11-22 17:29:21 -0800571 final List<Tile> tiles = category.getTiles();
Fan Zhange6c60c22016-10-04 17:48:32 -0700572 if (tiles == null) {
Jason Chiubac59872019-10-24 17:30:34 +0800573 Log.d(tag, "tile list is empty, skipping category " + category.key);
Fan Zhange6c60c22016-10-04 17:48:32 -0700574 return;
575 }
Fan Zhanga1a84e62016-10-19 14:15:34 -0700576 // Create a list to track which tiles are to be removed.
Jason Chiubac59872019-10-24 17:30:34 +0800577 final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
Fan Zhanga1a84e62016-10-19 14:15:34 -0700578
Jason Chiuc9615612022-05-06 11:14:54 +0800579 // Install dashboard tiles and collect pending observers.
Fan Zhang7d5a9ee2018-08-09 17:32:37 -0700580 final boolean forceRoundedIcons = shouldForceRoundedIcon();
Jason Chiuc9615612022-05-06 11:14:54 +0800581 final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200582
583 // Move group tiles to the beginning of the list to ensure they are created before the
584 // other tiles.
585 tiles.sort(Comparator.comparingInt(tile -> tile.getType() == Tile.Type.GROUP ? 0 : 1));
Fan Zhangbb6d2602016-10-04 13:21:06 -0700586 for (Tile tile : tiles) {
587 final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
588 if (TextUtils.isEmpty(key)) {
Jason Chiubac59872019-10-24 17:30:34 +0800589 Log.d(tag, "tile does not contain a key, skipping " + tile);
Fan Zhangbb6d2602016-10-04 13:21:06 -0700590 continue;
591 }
Doris Ling20d4b042016-11-22 16:37:06 -0800592 if (!displayTile(tile)) {
593 continue;
594 }
Jason Chiuc9615612022-05-06 11:14:54 +0800595 final List<DynamicDataObserver> observers;
Jason Chiubac59872019-10-24 17:30:34 +0800596 if (mDashboardTilePrefKeys.containsKey(key)) {
Fan Zhanga1a84e62016-10-19 14:15:34 -0700597 // Have the key already, will rebind.
Doris Ling8b14a1a2017-08-17 14:45:12 -0700598 final Preference preference = screen.findPreference(key);
Jason Chiuc9615612022-05-06 11:14:54 +0800599 observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
600 getActivity(), this, forceRoundedIcons, preference, tile, key,
Fan Zhang7d5a9ee2018-08-09 17:32:37 -0700601 mPlaceholderPreferenceController.getOrder());
Fan Zhanga1a84e62016-10-19 14:15:34 -0700602 } else {
603 // Don't have this key, add it.
Jason Chiubac59872019-10-24 17:30:34 +0800604 final Preference pref = createPreference(tile);
Jason Chiuc9615612022-05-06 11:14:54 +0800605 observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
606 getActivity(), this, forceRoundedIcons, pref, tile, key,
607 mPlaceholderPreferenceController.getOrder());
Edgar Wangcc0cd092025-06-20 07:21:45 +0000608 if (tile.hasGroupKey()) {
609 Preference group = screen.findPreference(tile.getGroupKey());
610 if (group instanceof PreferenceCategory) {
611 ((PreferenceCategory) group).addPreference(pref);
Edgar Wang9ff8dde2024-04-09 18:03:15 +0000612 } else {
613 screen.addPreference(pref);
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200614 }
615 } else {
Edgar Wangcc0cd092025-06-20 07:21:45 +0000616 screen.addPreference(pref);
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200617 }
Jason Chiubac59872019-10-24 17:30:34 +0800618 registerDynamicDataObservers(observers);
619 mDashboardTilePrefKeys.put(key, observers);
Fan Zhangbb6d2602016-10-04 13:21:06 -0700620 }
Jason Chiuc9615612022-05-06 11:14:54 +0800621 if (observers != null) {
622 pendingObservers.addAll(observers);
623 }
Fan Zhanga1a84e62016-10-19 14:15:34 -0700624 remove.remove(key);
Fan Zhangbb6d2602016-10-04 13:21:06 -0700625 }
Jason Chiuc9615612022-05-06 11:14:54 +0800626
627 // Remove tiles that are gone.
Jason Chiubac59872019-10-24 17:30:34 +0800628 for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
629 final String key = entry.getKey();
Fan Zhanga1a84e62016-10-19 14:15:34 -0700630 mDashboardTilePrefKeys.remove(key);
Edgar Wangcc0cd092025-06-20 07:21:45 +0000631 screen.removePreferenceRecursively(key);
Jason Chiubac59872019-10-24 17:30:34 +0800632 unregisterDynamicDataObservers(entry.getValue());
Fan Zhanga1a84e62016-10-19 14:15:34 -0700633 }
Jason Chiuc9615612022-05-06 11:14:54 +0800634
635 // Wait for pending observers to update UI.
636 if (!pendingObservers.isEmpty()) {
637 final CountDownLatch mainLatch = new CountDownLatch(1);
638 new Thread(() -> {
639 pendingObservers.forEach(observer ->
640 awaitObserverLatch(observer.getCountDownLatch()));
641 mainLatch.countDown();
642 }).start();
643 Log.d(tag, "Start waiting observers");
644 awaitObserverLatch(mainLatch);
645 Log.d(tag, "Stop waiting observers");
646 pendingObservers.forEach(DynamicDataObserver::updateUi);
647 }
Fan Zhanga1a84e62016-10-19 14:15:34 -0700648 }
jackqdyulei22904a02018-12-27 14:40:09 -0800649
650 @Override
651 public void onBlockerWorkFinished(BasePreferenceController controller) {
652 mBlockerController.countDown(controller.getPreferenceKey());
Yi-Ling Chuanga28770e2022-03-08 18:20:15 +0800653 controller.setUiBlockerFinished(mBlockerController.isBlockerFinished());
jackqdyulei22904a02018-12-27 14:40:09 -0800654 }
Jason Chiubac59872019-10-24 17:30:34 +0800655
Jason Chiu90eef242021-11-19 14:35:27 +0800656 protected Preference createPreference(Tile tile) {
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200657 switch (tile.getType()) {
658 case EXTERNAL_ACTION:
659 Preference externalActionPreference = new Preference(getPrefContext());
660 externalActionPreference
661 .setWidgetLayoutResource(R.layout.preference_external_action_icon);
662 return externalActionPreference;
663 case SWITCH:
Chaohui Wangcfdb3b12023-10-24 11:38:26 +0800664 return new SwitchPreferenceCompat(getPrefContext());
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200665 case SWITCH_WITH_ACTION:
666 return new PrimarySwitchPreference(getPrefContext());
667 case GROUP:
Peter Zhangf6a267b2023-05-10 16:01:58 +0200668 mMetricsFeatureProvider.action(
669 mMetricsFeatureProvider.getAttribution(getActivity()),
670 SettingsEnums.ACTION_SETTINGS_GROUP_TILE_ADDED_TO_SCREEN,
671 getMetricsCategory(),
672 tile.getKey(getContext()),
673 /* value= */ 0);
Peter Zhangbaeca6f2023-05-08 16:59:01 +0200674 return new PreferenceCategory((getPrefContext()));
675 case ACTION:
676 default:
677 return new Preference(getPrefContext());
Peter Zhangddb65e52023-05-07 23:55:50 +0200678 }
Jason Chiubac59872019-10-24 17:30:34 +0800679 }
680
681 @VisibleForTesting
682 void registerDynamicDataObservers(List<DynamicDataObserver> observers) {
683 if (observers == null || observers.isEmpty()) {
684 return;
685 }
686 final ContentResolver resolver = getContentResolver();
687 observers.forEach(observer -> registerDynamicDataObserver(resolver, observer));
688 }
689
690 private void registerDynamicDataObserver(ContentResolver resolver,
691 DynamicDataObserver observer) {
692 Log.d(TAG, "register observer: @" + Integer.toHexString(observer.hashCode())
693 + ", uri: " + observer.getUri());
Jason Chiuf22b2662024-06-25 20:55:35 +0800694 try {
695 resolver.registerContentObserver(observer.getUri(), false, observer);
696 mRegisteredObservers.add(observer);
697 } catch (Exception e) {
698 Log.w(TAG, "Cannot register observer: " + observer.getUri(), e);
699 }
Jason Chiubac59872019-10-24 17:30:34 +0800700 }
701
702 private void unregisterDynamicDataObservers(List<DynamicDataObserver> observers) {
703 if (observers == null || observers.isEmpty()) {
704 return;
705 }
706 final ContentResolver resolver = getContentResolver();
707 observers.forEach(observer -> {
708 Log.d(TAG, "unregister observer: @" + Integer.toHexString(observer.hashCode())
709 + ", uri: " + observer.getUri());
Jason Chiuf22b2662024-06-25 20:55:35 +0800710 if (mRegisteredObservers.remove(observer)) {
711 try {
712 resolver.unregisterContentObserver(observer);
713 } catch (Exception e) {
714 Log.w(TAG, "Cannot unregister observer: " + observer.getUri(), e);
715 }
716 }
Jason Chiubac59872019-10-24 17:30:34 +0800717 });
718 }
Jason Chiuc9615612022-05-06 11:14:54 +0800719
720 private void awaitObserverLatch(CountDownLatch latch) {
721 try {
722 latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
723 } catch (InterruptedException e) {
724 // Do nothing
725 }
726 }
Fan Zhangbb6d2602016-10-04 13:21:06 -0700727}