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