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.Context; 21 import android.os.Bundle; 22 import android.text.TextUtils; 23 import android.util.ArrayMap; 24 import android.util.ArraySet; 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 34 import com.android.settings.R; 35 import com.android.settings.SettingsPreferenceFragment; 36 import com.android.settings.core.BasePreferenceController; 37 import com.android.settings.core.PreferenceControllerListHelper; 38 import com.android.settings.core.SettingsBaseActivity; 39 import com.android.settings.overlay.FeatureFactory; 40 import com.android.settings.search.Indexable; 41 import com.android.settingslib.core.AbstractPreferenceController; 42 import com.android.settingslib.core.lifecycle.Lifecycle; 43 import com.android.settingslib.core.lifecycle.LifecycleObserver; 44 import com.android.settingslib.drawer.DashboardCategory; 45 import com.android.settingslib.drawer.Tile; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * Base fragment for dashboard style UI containing a list of static and dynamic setting items. 56 */ 57 public abstract class DashboardFragment extends SettingsPreferenceFragment 58 implements SettingsBaseActivity.CategoryListener, Indexable, 59 SummaryLoader.SummaryConsumer, PreferenceGroup.OnExpandButtonClickListener, 60 BasePreferenceController.UiBlockListener { 61 private static final String TAG = "DashboardFragment"; 62 63 private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers = 64 new ArrayMap<>(); 65 private final Set<String> mDashboardTilePrefKeys = new ArraySet<>(); 66 67 private DashboardFeatureProvider mDashboardFeatureProvider; 68 private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController; 69 private boolean mListeningToCategoryChange; 70 private SummaryLoader mSummaryLoader; 71 private List<String> mSuppressInjectedTileKeys; 72 @VisibleForTesting 73 UiBlockerController mBlockerController; 74 75 @Override onAttach(Context context)76 public void onAttach(Context context) { 77 super.onAttach(context); 78 mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray( 79 R.array.config_suppress_injected_tile_keys)); 80 mDashboardFeatureProvider = FeatureFactory.getFactory(context). 81 getDashboardFeatureProvider(context); 82 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 83 // Load preference controllers from code 84 final List<AbstractPreferenceController> controllersFromCode = 85 createPreferenceControllers(context); 86 // Load preference controllers from xml definition 87 final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper 88 .getPreferenceControllersFromXml(context, getPreferenceScreenResId()); 89 // Filter xml-based controllers in case a similar controller is created from code already. 90 final List<BasePreferenceController> uniqueControllerFromXml = 91 PreferenceControllerListHelper.filterControllers( 92 controllersFromXml, controllersFromCode); 93 94 // Add unique controllers to list. 95 if (controllersFromCode != null) { 96 controllers.addAll(controllersFromCode); 97 } 98 controllers.addAll(uniqueControllerFromXml); 99 100 // And wire up with lifecycle. 101 final Lifecycle lifecycle = getSettingsLifecycle(); 102 uniqueControllerFromXml 103 .stream() 104 .filter(controller -> controller instanceof LifecycleObserver) 105 .forEach( 106 controller -> lifecycle.addObserver((LifecycleObserver) controller)); 107 108 mPlaceholderPreferenceController = 109 new DashboardTilePlaceholderPreferenceController(context); 110 controllers.add(mPlaceholderPreferenceController); 111 for (AbstractPreferenceController controller : controllers) { 112 addPreferenceController(controller); 113 } 114 115 checkUiBlocker(controllers); 116 } 117 118 @VisibleForTesting checkUiBlocker(List<AbstractPreferenceController> controllers)119 void checkUiBlocker(List<AbstractPreferenceController> controllers) { 120 final List<String> keys = new ArrayList<>(); 121 controllers 122 .stream() 123 .filter(controller -> controller instanceof BasePreferenceController.UiBlocker) 124 .forEach(controller -> { 125 ((BasePreferenceController) controller).setUiBlockListener(this); 126 keys.add(controller.getPreferenceKey()); 127 }); 128 129 if (!keys.isEmpty()) { 130 mBlockerController = new UiBlockerController(keys); 131 mBlockerController.start(()->updatePreferenceVisibility(mPreferenceControllers)); 132 } 133 } 134 135 @Override onCreate(Bundle icicle)136 public void onCreate(Bundle icicle) { 137 super.onCreate(icicle); 138 // Set ComparisonCallback so we get better animation when list changes. 139 getPreferenceManager().setPreferenceComparisonCallback( 140 new PreferenceManager.SimplePreferenceComparisonCallback()); 141 if (icicle != null) { 142 // Upon rotation configuration change we need to update preference states before any 143 // editing dialog is recreated (that would happen before onResume is called). 144 updatePreferenceStates(); 145 } 146 } 147 148 @Override onCategoriesChanged()149 public void onCategoriesChanged() { 150 final DashboardCategory category = 151 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 152 if (category == null) { 153 return; 154 } 155 refreshDashboardTiles(getLogTag()); 156 } 157 158 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)159 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 160 refreshAllPreferences(getLogTag()); 161 } 162 163 @Override onStart()164 public void onStart() { 165 super.onStart(); 166 final DashboardCategory category = 167 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 168 if (category == null) { 169 return; 170 } 171 if (mSummaryLoader != null) { 172 // SummaryLoader can be null when there is no dynamic tiles. 173 mSummaryLoader.setListening(true); 174 } 175 final Activity activity = getActivity(); 176 if (activity instanceof SettingsBaseActivity) { 177 mListeningToCategoryChange = true; 178 ((SettingsBaseActivity) activity).addCategoryListener(this); 179 } 180 } 181 182 @Override notifySummaryChanged(Tile tile)183 public void notifySummaryChanged(Tile tile) { 184 final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); 185 final Preference pref = getPreferenceScreen().findPreference(key); 186 if (pref == null) { 187 Log.d(getLogTag(), String.format( 188 "Can't find pref by key %s, skipping update summary %s", 189 key, tile.getDescription())); 190 return; 191 } 192 pref.setSummary(tile.getSummary(pref.getContext())); 193 } 194 195 @Override onResume()196 public void onResume() { 197 super.onResume(); 198 updatePreferenceStates(); 199 } 200 201 @Override onPreferenceTreeClick(Preference preference)202 public boolean onPreferenceTreeClick(Preference preference) { 203 Collection<List<AbstractPreferenceController>> controllers = 204 mPreferenceControllers.values(); 205 // If preference contains intent, log it before handling. 206 mMetricsFeatureProvider.logDashboardStartIntent( 207 getContext(), preference.getIntent(), getMetricsCategory()); 208 // Give all controllers a chance to handle click. 209 for (List<AbstractPreferenceController> controllerList : controllers) { 210 for (AbstractPreferenceController controller : controllerList) { 211 if (controller.handlePreferenceTreeClick(preference)) { 212 return true; 213 } 214 } 215 } 216 return super.onPreferenceTreeClick(preference); 217 } 218 219 @Override onStop()220 public void onStop() { 221 super.onStop(); 222 if (mSummaryLoader != null) { 223 // SummaryLoader can be null when there is no dynamic tiles. 224 mSummaryLoader.setListening(false); 225 } 226 if (mListeningToCategoryChange) { 227 final Activity activity = getActivity(); 228 if (activity instanceof SettingsBaseActivity) { 229 ((SettingsBaseActivity) activity).remCategoryListener(this); 230 } 231 mListeningToCategoryChange = false; 232 } 233 } 234 235 @Override getPreferenceScreenResId()236 protected abstract int getPreferenceScreenResId(); 237 238 @Override onExpandButtonClick()239 public void onExpandButtonClick() { 240 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 241 SettingsEnums.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND, 242 getMetricsCategory(), null, 0); 243 } 244 shouldForceRoundedIcon()245 protected boolean shouldForceRoundedIcon() { 246 return false; 247 } 248 use(Class<T> clazz)249 protected <T extends AbstractPreferenceController> T use(Class<T> clazz) { 250 List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz); 251 if (controllerList != null) { 252 if (controllerList.size() > 1) { 253 Log.w(TAG, "Multiple controllers of Class " + clazz.getSimpleName() 254 + " found, returning first one."); 255 } 256 return (T) controllerList.get(0); 257 } 258 259 return null; 260 } 261 addPreferenceController(AbstractPreferenceController controller)262 protected void addPreferenceController(AbstractPreferenceController controller) { 263 if (mPreferenceControllers.get(controller.getClass()) == null) { 264 mPreferenceControllers.put(controller.getClass(), new ArrayList<>()); 265 } 266 mPreferenceControllers.get(controller.getClass()).add(controller); 267 } 268 269 /** 270 * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment. 271 */ 272 @VisibleForTesting getCategoryKey()273 public String getCategoryKey() { 274 return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName()); 275 } 276 277 /** 278 * Get the tag string for logging. 279 */ getLogTag()280 protected abstract String getLogTag(); 281 282 /** 283 * Get a list of {@link AbstractPreferenceController} for this fragment. 284 */ createPreferenceControllers(Context context)285 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 286 return null; 287 } 288 289 /** 290 * Returns true if this tile should be displayed 291 */ 292 @CallSuper displayTile(Tile tile)293 protected boolean displayTile(Tile tile) { 294 if (mSuppressInjectedTileKeys != null && tile.hasKey()) { 295 // For suppressing injected tiles for OEMs. 296 return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext())); 297 } 298 return true; 299 } 300 301 /** 302 * Displays resource based tiles. 303 */ displayResourceTiles()304 private void displayResourceTiles() { 305 final int resId = getPreferenceScreenResId(); 306 if (resId <= 0) { 307 return; 308 } 309 addPreferencesFromResource(resId); 310 final PreferenceScreen screen = getPreferenceScreen(); 311 screen.setOnExpandButtonClickListener(this); 312 mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach( 313 controller -> controller.displayPreference(screen)); 314 } 315 316 /** 317 * Get current PreferenceController(s) 318 */ getPreferenceControllers()319 protected Collection<List<AbstractPreferenceController>> getPreferenceControllers() { 320 return mPreferenceControllers.values(); 321 } 322 323 /** 324 * Update state of each preference managed by PreferenceController. 325 */ updatePreferenceStates()326 protected void updatePreferenceStates() { 327 final PreferenceScreen screen = getPreferenceScreen(); 328 Collection<List<AbstractPreferenceController>> controllerLists = 329 mPreferenceControllers.values(); 330 for (List<AbstractPreferenceController> controllerList : controllerLists) { 331 for (AbstractPreferenceController controller : controllerList) { 332 if (!controller.isAvailable()) { 333 continue; 334 } 335 336 final String key = controller.getPreferenceKey(); 337 if (TextUtils.isEmpty(key)) { 338 Log.d(TAG, String.format("Preference key is %s in Controller %s", 339 key, controller.getClass().getSimpleName())); 340 continue; 341 } 342 343 final Preference preference = screen.findPreference(key); 344 if (preference == null) { 345 Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s", 346 key, controller.getClass().getSimpleName())); 347 continue; 348 } 349 controller.updateState(preference); 350 } 351 } 352 } 353 354 /** 355 * Refresh all preference items, including both static prefs from xml, and dynamic items from 356 * DashboardCategory. 357 */ refreshAllPreferences(final String TAG)358 private void refreshAllPreferences(final String TAG) { 359 final PreferenceScreen screen = getPreferenceScreen(); 360 // First remove old preferences. 361 if (screen != null) { 362 // Intentionally do not cache PreferenceScreen because it will be recreated later. 363 screen.removeAll(); 364 } 365 366 // Add resource based tiles. 367 displayResourceTiles(); 368 369 refreshDashboardTiles(TAG); 370 371 final Activity activity = getActivity(); 372 if (activity != null) { 373 Log.d(TAG, "All preferences added, reporting fully drawn"); 374 activity.reportFullyDrawn(); 375 } 376 377 updatePreferenceVisibility(mPreferenceControllers); 378 } 379 380 @VisibleForTesting updatePreferenceVisibility( Map<Class, List<AbstractPreferenceController>> preferenceControllers)381 void updatePreferenceVisibility( 382 Map<Class, List<AbstractPreferenceController>> preferenceControllers) { 383 final PreferenceScreen screen = getPreferenceScreen(); 384 if (screen == null || preferenceControllers == null || mBlockerController == null) { 385 return; 386 } 387 388 final boolean visible = mBlockerController.isBlockerFinished(); 389 for (List<AbstractPreferenceController> controllerList : 390 preferenceControllers.values()) { 391 for (AbstractPreferenceController controller : controllerList) { 392 final String key = controller.getPreferenceKey(); 393 final Preference preference = findPreference(key); 394 if (preference != null) { 395 preference.setVisible(visible && controller.isAvailable()); 396 } 397 } 398 } 399 } 400 401 /** 402 * Refresh preference items backed by DashboardCategory. 403 */ 404 @VisibleForTesting refreshDashboardTiles(final String TAG)405 void refreshDashboardTiles(final String TAG) { 406 final PreferenceScreen screen = getPreferenceScreen(); 407 408 final DashboardCategory category = 409 mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); 410 if (category == null) { 411 Log.d(TAG, "NO dashboard tiles for " + TAG); 412 return; 413 } 414 final List<Tile> tiles = category.getTiles(); 415 if (tiles == null) { 416 Log.d(TAG, "tile list is empty, skipping category " + category.key); 417 return; 418 } 419 // Create a list to track which tiles are to be removed. 420 final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys); 421 422 // There are dashboard tiles, so we need to install SummaryLoader. 423 if (mSummaryLoader != null) { 424 mSummaryLoader.release(); 425 } 426 final Context context = getContext(); 427 mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey()); 428 mSummaryLoader.setSummaryConsumer(this); 429 // Install dashboard tiles. 430 final boolean forceRoundedIcons = shouldForceRoundedIcon(); 431 for (Tile tile : tiles) { 432 final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); 433 if (TextUtils.isEmpty(key)) { 434 Log.d(TAG, "tile does not contain a key, skipping " + tile); 435 continue; 436 } 437 if (!displayTile(tile)) { 438 continue; 439 } 440 if (mDashboardTilePrefKeys.contains(key)) { 441 // Have the key already, will rebind. 442 final Preference preference = screen.findPreference(key); 443 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons, 444 getMetricsCategory(), preference, tile, key, 445 mPlaceholderPreferenceController.getOrder()); 446 } else { 447 // Don't have this key, add it. 448 final Preference pref = new Preference(getPrefContext()); 449 mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons, 450 getMetricsCategory(), pref, tile, key, 451 mPlaceholderPreferenceController.getOrder()); 452 screen.addPreference(pref); 453 mDashboardTilePrefKeys.add(key); 454 } 455 remove.remove(key); 456 } 457 // Finally remove tiles that are gone. 458 for (String key : remove) { 459 mDashboardTilePrefKeys.remove(key); 460 final Preference preference = screen.findPreference(key); 461 if (preference != null) { 462 screen.removePreference(preference); 463 } 464 } 465 mSummaryLoader.setListening(true); 466 } 467 468 @Override onBlockerWorkFinished(BasePreferenceController controller)469 public void onBlockerWorkFinished(BasePreferenceController controller) { 470 mBlockerController.countDown(controller.getPreferenceKey()); 471 } 472 } 473