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