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