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