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