• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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  */
16 package com.android.settings.dashboard;
17 
18 import android.app.Activity;
19 import android.app.settings.SettingsEnums;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.preference.PreferenceManager.OnActivityResultListener;
25 import android.text.TextUtils;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 import android.view.View;
29 
30 import androidx.annotation.CallSuper;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 import androidx.lifecycle.LifecycleObserver;
35 import androidx.lifecycle.LifecycleOwner;
36 import androidx.preference.Preference;
37 import androidx.preference.PreferenceCategory;
38 import androidx.preference.PreferenceGroup;
39 import androidx.preference.PreferenceManager;
40 import androidx.preference.PreferenceScreen;
41 import androidx.preference.SwitchPreferenceCompat;
42 
43 import com.android.settings.R;
44 import com.android.settings.SettingsPreferenceFragment;
45 import com.android.settings.core.BasePreferenceController;
46 import com.android.settings.core.CategoryMixin.CategoryHandler;
47 import com.android.settings.core.CategoryMixin.CategoryListener;
48 import com.android.settings.core.PreferenceControllerListHelper;
49 import com.android.settings.flags.Flags;
50 import com.android.settings.overlay.FeatureFactory;
51 import com.android.settingslib.PrimarySwitchPreference;
52 import com.android.settingslib.core.AbstractPreferenceController;
53 import com.android.settingslib.core.lifecycle.Lifecycle;
54 import com.android.settingslib.drawer.DashboardCategory;
55 import com.android.settingslib.drawer.Tile;
56 import com.android.settingslib.preference.PreferenceScreenCreator;
57 import com.android.settingslib.search.Indexable;
58 
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.Collection;
62 import java.util.Collections;
63 import java.util.Comparator;
64 import java.util.Iterator;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Objects;
68 import java.util.Set;
69 import java.util.concurrent.CountDownLatch;
70 import java.util.concurrent.TimeUnit;
71 
72 /**
73  * Base fragment for dashboard style UI containing a list of static and dynamic setting items.
74  */
75 public abstract class DashboardFragment extends SettingsPreferenceFragment
76         implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
77         BasePreferenceController.UiBlockListener {
78     public static final String CATEGORY = "category";
79     private static final String TAG = "DashboardFragment";
80     private static final long TIMEOUT_MILLIS = 50L;
81 
82     @VisibleForTesting
83     final ArrayMap<String, List<DynamicDataObserver>> mDashboardTilePrefKeys = new ArrayMap<>();
84     private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers =
85             new ArrayMap<>();
86     private final List<DynamicDataObserver> mRegisteredObservers = new ArrayList<>();
87     private final List<AbstractPreferenceController> mControllers = new ArrayList<>();
88     @VisibleForTesting
89     UiBlockerController mBlockerController;
90     private DashboardFeatureProvider mDashboardFeatureProvider;
91     private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController;
92     private boolean mListeningToCategoryChange;
93     private List<String> mSuppressInjectedTileKeys;
94 
95     @Override
onAttach(Context context)96     public void onAttach(Context context) {
97         super.onAttach(context);
98         mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray(
99                 R.array.config_suppress_injected_tile_keys));
100         mDashboardFeatureProvider =
101                 FeatureFactory.getFeatureFactory().getDashboardFeatureProvider();
102 
103         PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
104         if (preferenceScreenCreator == null || !preferenceScreenCreator.hasCompleteHierarchy()) {
105             // Load preference controllers from code
106             final List<AbstractPreferenceController> controllersFromCode =
107                     createPreferenceControllers(context);
108             // Load preference controllers from xml definition
109             final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
110                     .getPreferenceControllersFromXml(context, getPreferenceScreenResId());
111             // Filter xml-based controllers in case a similar controller is created from code
112             // already.
113             final List<BasePreferenceController> uniqueControllerFromXml =
114                     PreferenceControllerListHelper.filterControllers(
115                             controllersFromXml, controllersFromCode);
116 
117             // Add unique controllers to list.
118             if (controllersFromCode != null) {
119                 mControllers.addAll(controllersFromCode);
120             }
121             mControllers.addAll(uniqueControllerFromXml);
122 
123             // And wire up with lifecycle.
124             final Lifecycle lifecycle = getSettingsLifecycle();
125             uniqueControllerFromXml.forEach(controller -> {
126                 if (controller instanceof LifecycleObserver) {
127                     lifecycle.addObserver((LifecycleObserver) controller);
128                 }
129             });
130         }
131 
132         // Set metrics category for BasePreferenceController.
133         final int metricCategory = getMetricsCategory();
134         mControllers.forEach(controller -> {
135             if (controller instanceof BasePreferenceController) {
136                 ((BasePreferenceController) controller).setMetricsCategory(metricCategory);
137             }
138         });
139 
140         mPlaceholderPreferenceController =
141                 new DashboardTilePlaceholderPreferenceController(context);
142         mControllers.add(mPlaceholderPreferenceController);
143         for (AbstractPreferenceController controller : mControllers) {
144             addPreferenceController(controller);
145         }
146     }
147 
148     @VisibleForTesting
checkUiBlocker(List<AbstractPreferenceController> controllers)149     void checkUiBlocker(List<AbstractPreferenceController> controllers) {
150         final List<String> keys = new ArrayList<>();
151         final List<BasePreferenceController> baseControllers = new ArrayList<>();
152         controllers.forEach(controller -> {
153             if (controller instanceof BasePreferenceController.UiBlocker
154                     && controller.isAvailable()) {
155                 ((BasePreferenceController) controller).setUiBlockListener(this);
156                 keys.add(controller.getPreferenceKey());
157                 baseControllers.add((BasePreferenceController) controller);
158             }
159         });
160 
161         if (!keys.isEmpty()) {
162             mBlockerController = new UiBlockerController(keys);
163             mBlockerController.start(() -> {
164                 updatePreferenceVisibility(mPreferenceControllers);
165                 baseControllers.forEach(controller -> controller.setUiBlockerFinished(true));
166             });
167         }
168     }
169 
170     @Override
onCreate(Bundle icicle)171     public void onCreate(Bundle icicle) {
172         super.onCreate(icicle);
173         // Set ComparisonCallback so we get better animation when list changes.
174         getPreferenceManager().setPreferenceComparisonCallback(
175                 new PreferenceManager.SimplePreferenceComparisonCallback());
176         if (icicle != null) {
177             // Upon rotation configuration change we need to update preference states before any
178             // editing dialog is recreated (that would happen before onResume is called).
179             updatePreferenceStates();
180         }
181     }
182 
183     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)184     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
185         super.onViewCreated(view, savedInstanceState);
186         LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
187         for (AbstractPreferenceController controller : mControllers) {
188             controller.onViewCreated(viewLifecycleOwner);
189         }
190     }
191 
192     @Override
onCategoriesChanged(Set<String> categories)193     public void onCategoriesChanged(Set<String> categories) {
194         final String categoryKey = getCategoryKey();
195         final DashboardCategory dashboardCategory =
196                 mDashboardFeatureProvider.getTilesForCategory(categoryKey);
197         if (dashboardCategory == null) {
198             return;
199         }
200 
201         if (categories == null) {
202             // force refreshing
203             refreshDashboardTiles(getLogTag());
204         } else if (categories.contains(categoryKey)) {
205             Log.i(TAG, "refresh tiles for " + categoryKey);
206             refreshDashboardTiles(getLogTag());
207         }
208     }
209 
210     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)211     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
212         checkUiBlocker(mControllers);
213         refreshAllPreferences(getLogTag());
214         mControllers.stream()
215                 .map(controller -> (Preference) findPreference(controller.getPreferenceKey()))
216                 .filter(Objects::nonNull)
217                 .forEach(preference -> {
218                     // Give all controllers a chance to handle click.
219                     preference.getExtras().putInt(CATEGORY, getMetricsCategory());
220                 });
221     }
222 
223     @Override
onStart()224     public void onStart() {
225         super.onStart();
226         final DashboardCategory category =
227                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
228         if (category == null) {
229             return;
230         }
231         final Activity activity = getActivity();
232         if (activity instanceof CategoryHandler) {
233             mListeningToCategoryChange = true;
234             ((CategoryHandler) activity).getCategoryMixin().addCategoryListener(this);
235         }
236         final ContentResolver resolver = getContentResolver();
237         mDashboardTilePrefKeys.values().stream()
238                 .filter(Objects::nonNull)
239                 .flatMap(List::stream)
240                 .forEach(observer -> {
241                     if (!mRegisteredObservers.contains(observer)) {
242                         registerDynamicDataObserver(resolver, observer);
243                     }
244                 });
245     }
246 
247     @Override
onResume()248     public void onResume() {
249         super.onResume();
250         updatePreferenceStates();
251     }
252 
253     @Override
onPreferenceTreeClick(Preference preference)254     public boolean onPreferenceTreeClick(Preference preference) {
255         if (isCatalystEnabled()) {
256             Intent intent = preference.getIntent();
257             if (intent != null && preference.getContext().getPackageManager().queryIntentActivities(
258                     intent, 0).isEmpty()) {
259                 Log.w(TAG, "No activity to start for " + intent);
260                 return true;
261             }
262         }
263 
264         final Collection<List<AbstractPreferenceController>> controllers =
265                 mPreferenceControllers.values();
266         for (List<AbstractPreferenceController> controllerList : controllers) {
267             for (AbstractPreferenceController controller : controllerList) {
268                 if (controller.handlePreferenceTreeClick(preference)) {
269                     // log here since calling super.onPreferenceTreeClick will be skipped
270                     writePreferenceClickMetric(preference);
271                     return true;
272                 }
273             }
274         }
275         return super.onPreferenceTreeClick(preference);
276     }
277 
278     @Override
onStop()279     public void onStop() {
280         super.onStop();
281         unregisterDynamicDataObservers(new ArrayList<>(mRegisteredObservers));
282         if (mListeningToCategoryChange) {
283             final Activity activity = getActivity();
284             if (activity instanceof CategoryHandler) {
285                 ((CategoryHandler) activity).getCategoryMixin().removeCategoryListener(this);
286             }
287             mListeningToCategoryChange = false;
288         }
289     }
290 
291     @Override
getPreferenceScreenResId()292     protected abstract int getPreferenceScreenResId();
293 
294     @Override
onExpandButtonClick()295     public void onExpandButtonClick() {
296         mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN,
297                 SettingsEnums.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND,
298                 getMetricsCategory(), null, 0);
299     }
300 
301     @Override
onActivityResult(int requestCode, int resultCode, @Nullable Intent data)302     public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
303         for (List<AbstractPreferenceController> controllerList : mPreferenceControllers.values()) {
304             for (AbstractPreferenceController controller : controllerList) {
305                 if (controller instanceof OnActivityResultListener) {
306                     ((OnActivityResultListener) controller).onActivityResult(
307                             requestCode, resultCode, data);
308                 }
309             }
310         }
311         super.onActivityResult(requestCode, resultCode, data);
312     }
313 
shouldForceRoundedIcon()314     protected boolean shouldForceRoundedIcon() {
315         return false;
316     }
317 
use(Class<T> clazz)318     protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
319         List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);
320         if (controllerList != null) {
321             if (controllerList.size() > 1) {
322                 Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName()
323                         + " found, returning first one.");
324             }
325             return (T) controllerList.get(0);
326         }
327 
328         return null;
329     }
330 
331     /** Returns grouped controllers of input type T. */
useGroup( Class<T> clazz)332     protected <T extends AbstractPreferenceController> List<AbstractPreferenceController> useGroup(
333             Class<T> clazz) {
334         return mPreferenceControllers.values().stream().flatMap(Collection::stream).filter(
335                 controller -> clazz.isInstance(controller)).toList();
336     }
337 
338     /** Returns all controllers of type T. */
useAll(Class<T> clazz)339     protected <T extends AbstractPreferenceController> List<T> useAll(Class<T> clazz) {
340         return (List<T>) mPreferenceControllers.getOrDefault(clazz, Collections.emptyList());
341     }
342 
addPreferenceController(AbstractPreferenceController controller)343     protected void addPreferenceController(AbstractPreferenceController controller) {
344         if (mPreferenceControllers.get(controller.getClass()) == null) {
345             mPreferenceControllers.put(controller.getClass(), new ArrayList<>());
346         }
347         mPreferenceControllers.get(controller.getClass()).add(controller);
348     }
349 
350     /**
351      * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
352      */
353     @VisibleForTesting
getCategoryKey()354     public String getCategoryKey() {
355         return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
356     }
357 
358     /**
359      * Get the tag string for logging.
360      */
getLogTag()361     protected abstract String getLogTag();
362 
363     /**
364      * Get a list of {@link AbstractPreferenceController} for this fragment.
365      */
createPreferenceControllers(Context context)366     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
367         return null;
368     }
369 
370     /**
371      * Returns true if this tile should be displayed
372      */
373     @CallSuper
displayTile(Tile tile)374     protected boolean displayTile(Tile tile) {
375         if (mSuppressInjectedTileKeys != null && tile.hasKey()) {
376             // For suppressing injected tiles for OEMs.
377             return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext()));
378         }
379         return true;
380     }
381 
382     /**
383      * Displays resource based tiles.
384      */
displayResourceTiles()385     private void displayResourceTiles() {
386         PreferenceScreen screen;
387         PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
388         if (preferenceScreenCreator != null) {
389             screen = createPreferenceScreen();
390             if (!preferenceScreenCreator.hasCompleteHierarchy()) {
391                 removeControllersForHybridMode();
392             }
393             setPreferenceScreen(screen);
394         } else {
395             final int resId = getPreferenceScreenResId();
396             if (resId <= 0) {
397                 return;
398             }
399             addPreferencesFromResource(resId);
400             screen = getPreferenceScreen();
401         }
402         screen.setOnExpandButtonClickListener(this);
403         displayResourceTilesToScreen(screen);
404     }
405 
406     /**
407      * Removes preference controllers that have been migrated to catalyst.
408      *
409      * In hybrid mode, preference screen is inflated from XML resource, while preference metadata
410      * in the preference hierarchy are used to update preference widget UI. To avoid conflict,
411      * remove the preference controllers.
412      */
removeControllersForHybridMode()413     private void removeControllersForHybridMode() {
414         Set<String> keys = getPreferenceKeysInHierarchy();
415         Iterator<AbstractPreferenceController> iterator = mControllers.iterator();
416         Lifecycle lifecycle = getSettingsLifecycle();
417         while (iterator.hasNext()) {
418             AbstractPreferenceController controller = iterator.next();
419             String key = controller.getPreferenceKey();
420             if (keys.contains(key)) {
421                 Log.i(TAG, "Remove preference controller for " + key);
422                 iterator.remove();
423                 List<AbstractPreferenceController> controllers = mPreferenceControllers.get(
424                         controller.getClass());
425                 if (controllers != null) {
426                     controllers.remove(controller);
427                 }
428                 if (controller instanceof LifecycleObserver) {
429                     lifecycle.removeObserver((LifecycleObserver) controller);
430                 }
431             }
432         }
433     }
434 
435     /**
436      * Perform {@link AbstractPreferenceController#displayPreference(PreferenceScreen)}
437      * on all {@link AbstractPreferenceController}s.
438      */
displayResourceTilesToScreen(PreferenceScreen screen)439     protected void displayResourceTilesToScreen(PreferenceScreen screen) {
440         mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach(
441                 controller -> controller.displayPreference(screen));
442     }
443 
444     /**
445      * Get current PreferenceController(s)
446      */
getPreferenceControllers()447     protected Collection<List<AbstractPreferenceController>> getPreferenceControllers() {
448         return mPreferenceControllers.values();
449     }
450 
451     /**
452      * Update state of each preference managed by PreferenceController.
453      */
updatePreferenceStates()454     protected void updatePreferenceStates() {
455         final PreferenceScreen screen = getPreferenceScreen();
456         Collection<List<AbstractPreferenceController>> controllerLists =
457                 mPreferenceControllers.values();
458         for (List<AbstractPreferenceController> controllerList : controllerLists) {
459             for (AbstractPreferenceController controller : controllerList) {
460                 if (!controller.isAvailable()) {
461                     continue;
462                 }
463 
464                 final String key = controller.getPreferenceKey();
465                 if (TextUtils.isEmpty(key)) {
466                     Log.d(TAG, String.format("Preference key is %s in Controller %s",
467                             key, controller.getClass().getSimpleName()));
468                     continue;
469                 }
470 
471                 final Preference preference = screen.findPreference(key);
472                 if (preference == null) {
473                     Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s",
474                             key, controller.getClass().getSimpleName()));
475                     continue;
476                 }
477                 controller.updateState(preference);
478             }
479         }
480     }
481 
482     /**
483      * Refresh all preference items, including both static prefs from xml, and dynamic items from
484      * DashboardCategory.
485      */
refreshAllPreferences(final String tag)486     private void refreshAllPreferences(final String tag) {
487         final PreferenceScreen screen = getPreferenceScreen();
488         // First remove old preferences.
489         if (screen != null) {
490             // Intentionally do not cache PreferenceScreen because it will be recreated later.
491             screen.removeAll();
492         }
493 
494         // Add resource based tiles.
495         displayResourceTiles();
496 
497         refreshDashboardTiles(tag);
498 
499         final Activity activity = getActivity();
500         if (activity != null) {
501             Log.d(tag, "All preferences added, reporting fully drawn");
502             activity.reportFullyDrawn();
503         }
504 
505         updatePreferenceVisibility(mPreferenceControllers);
506     }
507 
508     /**
509      * Force update all the preferences in this fragment.
510      */
forceUpdatePreferences()511     public void forceUpdatePreferences() {
512         final PreferenceScreen screen = getPreferenceScreen();
513         if (screen == null || mPreferenceControllers == null) {
514             return;
515         }
516         for (List<AbstractPreferenceController> controllerList : mPreferenceControllers.values()) {
517             for (AbstractPreferenceController controller : controllerList) {
518                 final String key = controller.getPreferenceKey();
519                 final Preference preference = findPreference(key);
520                 if (preference == null) {
521                     continue;
522                 }
523                 final boolean available = controller.isAvailable();
524                 if (available) {
525                     controller.updateState(preference);
526                 }
527                 preference.setVisible(available);
528             }
529         }
530     }
531 
532     @VisibleForTesting
updatePreferenceVisibility( Map<Class, List<AbstractPreferenceController>> preferenceControllers)533     void updatePreferenceVisibility(
534             Map<Class, List<AbstractPreferenceController>> preferenceControllers) {
535         final PreferenceScreen screen = getPreferenceScreen();
536         if (screen == null || preferenceControllers == null || mBlockerController == null) {
537             return;
538         }
539 
540         final boolean visible = mBlockerController.isBlockerFinished();
541         for (List<AbstractPreferenceController> controllerList :
542                 preferenceControllers.values()) {
543             for (AbstractPreferenceController controller : controllerList) {
544                 final String key = controller.getPreferenceKey();
545                 final Preference preference = findPreference(key);
546                 if (preference == null) {
547                     continue;
548                 }
549                 if (controller instanceof BasePreferenceController.UiBlocker) {
550                     final boolean prefVisible =
551                             ((BasePreferenceController) controller).getSavedPrefVisibility();
552                     preference.setVisible(visible && controller.isAvailable() && prefVisible);
553                 } else {
554                     preference.setVisible(visible && controller.isAvailable());
555                 }
556             }
557         }
558     }
559 
560     /**
561      * Refresh preference items backed by DashboardCategory.
562      */
refreshDashboardTiles(final String tag)563     private void refreshDashboardTiles(final String tag) {
564         final PreferenceScreen screen = getPreferenceScreen();
565 
566         final DashboardCategory category =
567                 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
568         if (category == null) {
569             Log.d(tag, "NO dashboard tiles for " + tag);
570             return;
571         }
572         final List<Tile> tiles = category.getTiles();
573         if (tiles == null) {
574             Log.d(tag, "tile list is empty, skipping category " + category.key);
575             return;
576         }
577         // Create a list to track which tiles are to be removed.
578         final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
579 
580         // Install dashboard tiles and collect pending observers.
581         final boolean forceRoundedIcons = shouldForceRoundedIcon();
582         final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
583 
584         // Move group tiles to the beginning of the list to ensure they are created before the
585         // other tiles.
586         tiles.sort(Comparator.comparingInt(tile -> tile.getType() == Tile.Type.GROUP ? 0 : 1));
587         for (Tile tile : tiles) {
588             final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
589             if (TextUtils.isEmpty(key)) {
590                 Log.d(tag, "tile does not contain a key, skipping " + tile);
591                 continue;
592             }
593             if (!displayTile(tile)) {
594                 continue;
595             }
596             final List<DynamicDataObserver> observers;
597             if (mDashboardTilePrefKeys.containsKey(key)) {
598                 // Have the key already, will rebind.
599                 final Preference preference = screen.findPreference(key);
600                 observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
601                         getActivity(), this, forceRoundedIcons, preference, tile, key,
602                         mPlaceholderPreferenceController.getOrder());
603             } else {
604                 // Don't have this key, add it.
605                 final Preference pref = createPreference(tile);
606                 observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
607                         getActivity(), this, forceRoundedIcons, pref, tile, key,
608                         mPlaceholderPreferenceController.getOrder());
609                 if (Flags.dynamicInjectionCategory()) {
610                     if (tile.hasGroupKey()) {
611                         Preference group = screen.findPreference(tile.getGroupKey());
612                         if (group instanceof PreferenceCategory) {
613                             ((PreferenceCategory) group).addPreference(pref);
614                         } else {
615                             screen.addPreference(pref);
616                         }
617                     } else {
618                         screen.addPreference(pref);
619                     }
620                 } else {
621                     if (tile.hasGroupKey()
622                             && mDashboardTilePrefKeys.containsKey(tile.getGroupKey())) {
623                         Preference group = screen.findPreference(tile.getGroupKey());
624                         if (group instanceof PreferenceCategory) {
625                             ((PreferenceCategory) group).addPreference(pref);
626                         }
627                     } else {
628                         screen.addPreference(pref);
629                     }
630                 }
631                 registerDynamicDataObservers(observers);
632                 mDashboardTilePrefKeys.put(key, observers);
633             }
634             if (observers != null) {
635                 pendingObservers.addAll(observers);
636             }
637             remove.remove(key);
638         }
639 
640         // Remove tiles that are gone.
641         for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
642             final String key = entry.getKey();
643             mDashboardTilePrefKeys.remove(key);
644             if (Flags.dynamicInjectionCategory()) {
645                 screen.removePreferenceRecursively(key);
646             } else {
647                 Preference preference = screen.findPreference(key);
648                 if (preference != null) {
649                     screen.removePreference(preference);
650                 }
651             }
652             unregisterDynamicDataObservers(entry.getValue());
653         }
654 
655         // Wait for pending observers to update UI.
656         if (!pendingObservers.isEmpty()) {
657             final CountDownLatch mainLatch = new CountDownLatch(1);
658             new Thread(() -> {
659                 pendingObservers.forEach(observer ->
660                         awaitObserverLatch(observer.getCountDownLatch()));
661                 mainLatch.countDown();
662             }).start();
663             Log.d(tag, "Start waiting observers");
664             awaitObserverLatch(mainLatch);
665             Log.d(tag, "Stop waiting observers");
666             pendingObservers.forEach(DynamicDataObserver::updateUi);
667         }
668     }
669 
670     @Override
onBlockerWorkFinished(BasePreferenceController controller)671     public void onBlockerWorkFinished(BasePreferenceController controller) {
672         mBlockerController.countDown(controller.getPreferenceKey());
673         controller.setUiBlockerFinished(mBlockerController.isBlockerFinished());
674     }
675 
createPreference(Tile tile)676     protected Preference createPreference(Tile tile) {
677         switch (tile.getType()) {
678             case EXTERNAL_ACTION:
679                 Preference externalActionPreference = new Preference(getPrefContext());
680                 externalActionPreference
681                         .setWidgetLayoutResource(R.layout.preference_external_action_icon);
682                 return externalActionPreference;
683             case SWITCH:
684                 return new SwitchPreferenceCompat(getPrefContext());
685             case SWITCH_WITH_ACTION:
686                 return new PrimarySwitchPreference(getPrefContext());
687             case GROUP:
688                 mMetricsFeatureProvider.action(
689                         mMetricsFeatureProvider.getAttribution(getActivity()),
690                         SettingsEnums.ACTION_SETTINGS_GROUP_TILE_ADDED_TO_SCREEN,
691                         getMetricsCategory(),
692                         tile.getKey(getContext()),
693                         /* value= */ 0);
694                 return new PreferenceCategory((getPrefContext()));
695             case ACTION:
696             default:
697                 return new Preference(getPrefContext());
698         }
699     }
700 
701     @VisibleForTesting
registerDynamicDataObservers(List<DynamicDataObserver> observers)702     void registerDynamicDataObservers(List<DynamicDataObserver> observers) {
703         if (observers == null || observers.isEmpty()) {
704             return;
705         }
706         final ContentResolver resolver = getContentResolver();
707         observers.forEach(observer -> registerDynamicDataObserver(resolver, observer));
708     }
709 
registerDynamicDataObserver(ContentResolver resolver, DynamicDataObserver observer)710     private void registerDynamicDataObserver(ContentResolver resolver,
711             DynamicDataObserver observer) {
712         Log.d(TAG, "register observer: @" + Integer.toHexString(observer.hashCode())
713                 + ", uri: " + observer.getUri());
714         try {
715             resolver.registerContentObserver(observer.getUri(), false, observer);
716             mRegisteredObservers.add(observer);
717         } catch (Exception e) {
718             Log.w(TAG, "Cannot register observer: " + observer.getUri(), e);
719         }
720     }
721 
unregisterDynamicDataObservers(List<DynamicDataObserver> observers)722     private void unregisterDynamicDataObservers(List<DynamicDataObserver> observers) {
723         if (observers == null || observers.isEmpty()) {
724             return;
725         }
726         final ContentResolver resolver = getContentResolver();
727         observers.forEach(observer -> {
728             Log.d(TAG, "unregister observer: @" + Integer.toHexString(observer.hashCode())
729                     + ", uri: " + observer.getUri());
730             if (mRegisteredObservers.remove(observer)) {
731                 try {
732                     resolver.unregisterContentObserver(observer);
733                 } catch (Exception e) {
734                     Log.w(TAG, "Cannot unregister observer: " + observer.getUri(), e);
735                 }
736             }
737         });
738     }
739 
awaitObserverLatch(CountDownLatch latch)740     private void awaitObserverLatch(CountDownLatch latch) {
741         try {
742             latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
743         } catch (InterruptedException e) {
744             // Do nothing
745         }
746     }
747 }
748