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