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