1 /* 2 * Copyright 2018 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 17 package com.android.car.settings.common; 18 19 import android.car.drivingstate.CarUxRestrictions; 20 import android.car.drivingstate.CarUxRestrictionsManager.OnUxRestrictionsChangedListener; 21 import android.content.Context; 22 import android.widget.Toast; 23 24 import androidx.annotation.IntDef; 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import androidx.lifecycle.DefaultLifecycleObserver; 28 import androidx.lifecycle.LifecycleOwner; 29 import androidx.preference.Preference; 30 import androidx.preference.PreferenceGroup; 31 32 import com.android.car.settings.R; 33 import com.android.car.ui.preference.ClickableWhileDisabledPreference; 34 import com.android.car.ui.preference.UxRestrictablePreference; 35 36 import java.lang.annotation.Retention; 37 import java.lang.annotation.RetentionPolicy; 38 import java.util.Arrays; 39 import java.util.HashSet; 40 import java.util.Set; 41 import java.util.function.Consumer; 42 43 /** 44 * Controller which encapsulates the business logic associated with a {@link Preference}. All car 45 * settings controllers should extend this class. 46 * 47 * <p>Controllers are responsible for populating and modifying the presentation of an associated 48 * preference while responding to changes in system state. This is enabled via {@link 49 * SettingsFragment} which registers controllers as observers on its lifecycle and dispatches 50 * {@link CarUxRestrictions} change events to the controllers via the {@link 51 * OnUxRestrictionsChangedListener} interface. 52 * 53 * <p>Controllers should be instantiated from XML. To do so, define a preference and include the 54 * {@code controller} attribute in the preference tag and assign the fully qualified class name. 55 * 56 * <p>For example: 57 * <pre>{@code 58 * <Preference 59 * android:key="my_preference_key" 60 * android:title="@string/my_preference_title" 61 * android:icon="@drawable/ic_settings" 62 * android:fragment="com.android.settings.foo.MyFragment" 63 * settings:controller="com.android.settings.foo.MyPreferenceController"/> 64 * }</pre> 65 * 66 * <p>Subclasses must implement {@link #getPreferenceType()} to define the upper bound type on the 67 * {@link Preference} that the controller is associated with. For example, a bound of {@link 68 * androidx.preference.PreferenceGroup} indicates that the controller will utilize preference group 69 * methods in its operation. {@link #setPreference(Preference)} will throw an {@link 70 * IllegalArgumentException} if not passed a subclass of the upper bound type. 71 * 72 * <p>Subclasses may implement any or all of the following methods (see method Javadocs for more 73 * information): 74 * 75 * <ul> 76 * <li>{@link #checkInitialized()} 77 * <li>{@link #onCreateInternal()} 78 * <li>{@link #getAvailabilityStatus()} 79 * <li>{@link #onStartInternal()} 80 * <li>{@link #onResumeInternal()} 81 * <li>{@link #onPauseInternal()} 82 * <li>{@link #onStopInternal()} 83 * <li>{@link #onDestroyInternal()} 84 * <li>{@link #updateState(Preference)} 85 * <li>{@link #onApplyUxRestrictions(CarUxRestrictions)} 86 * <li>{@link #handlePreferenceChanged(Preference, Object)} 87 * <li>{@link #handlePreferenceClicked(Preference)} 88 * </ul> 89 * 90 * @param <V> the upper bound on the type of {@link Preference} on which the controller 91 * expects to operate. 92 */ 93 public abstract class PreferenceController<V extends Preference> implements 94 DefaultLifecycleObserver, 95 OnUxRestrictionsChangedListener { 96 97 /** 98 * Denotes the availability of a setting. 99 * 100 * @see #getAvailabilityStatus() 101 */ 102 @Retention(RetentionPolicy.SOURCE) 103 @IntDef({AVAILABLE, CONDITIONALLY_UNAVAILABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_PROFILE, 104 AVAILABLE_FOR_VIEWING}) 105 public @interface AvailabilityStatus { 106 } 107 108 /** 109 * The setting is available. 110 */ 111 public static final int AVAILABLE = 0; 112 113 /** 114 * The setting is currently unavailable but may become available in the future. Use 115 * {@link #DISABLED_FOR_PROFILE} if it describes the condition more accurately. 116 */ 117 public static final int CONDITIONALLY_UNAVAILABLE = 1; 118 119 /** 120 * The setting is not and will not be supported by this device. 121 */ 122 public static final int UNSUPPORTED_ON_DEVICE = 2; 123 124 /** 125 * The setting cannot be changed by the current profile. 126 */ 127 public static final int DISABLED_FOR_PROFILE = 3; 128 129 /** 130 * The setting cannot be changed. 131 */ 132 public static final int AVAILABLE_FOR_VIEWING = 4; 133 134 /** 135 * Indicates whether all Preferences are configured to ignore UX Restrictions Event. 136 */ 137 private final boolean mAlwaysIgnoreUxRestrictions; 138 139 /** 140 * Set of the keys of Preferences that ignore UX Restrictions. When mAlwaysIgnoreUxRestrictions 141 * is configured to be false, then only the Preferences whose keys are contained in this Set 142 * ignore UX Restrictions. 143 */ 144 private final Set<String> mPreferencesIgnoringUxRestrictions; 145 146 private final Context mContext; 147 private final String mPreferenceKey; 148 private final FragmentController mFragmentController; 149 private final String mRestrictedWhileDrivingMessage; 150 151 private CarUxRestrictions mUxRestrictions; 152 private V mPreference; 153 private boolean mIsCreated; 154 private boolean mIsStarted; 155 156 /** 157 * Controllers should be instantiated from XML. To pass additional arguments see 158 * {@link SettingsFragment#use(Class, int)}. 159 */ PreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)160 public PreferenceController(Context context, String preferenceKey, 161 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 162 mContext = context; 163 mPreferenceKey = preferenceKey; 164 mFragmentController = fragmentController; 165 mUxRestrictions = uxRestrictions; 166 mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList( 167 mContext.getResources().getStringArray(R.array.config_ignore_ux_restrictions))); 168 mAlwaysIgnoreUxRestrictions = 169 mContext.getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions); 170 mRestrictedWhileDrivingMessage = 171 mContext.getResources().getString(R.string.car_ui_restricted_while_driving); 172 } 173 174 /** 175 * Returns the context used to construct the controller. 176 */ getContext()177 protected final Context getContext() { 178 return mContext; 179 } 180 181 /** 182 * Returns the key for the preference managed by this controller set at construction. 183 */ getPreferenceKey()184 protected final String getPreferenceKey() { 185 return mPreferenceKey; 186 } 187 188 /** 189 * Returns the {@link FragmentController} used to launch fragments and go back to previous 190 * fragments. This is set at construction. 191 */ getFragmentController()192 protected final FragmentController getFragmentController() { 193 return mFragmentController; 194 } 195 196 /** 197 * Returns the current {@link CarUxRestrictions} applied to the controller. Subclasses may use 198 * this to limit which content is displayed in the associated preference. May be called anytime. 199 */ getUxRestrictions()200 protected final CarUxRestrictions getUxRestrictions() { 201 return mUxRestrictions; 202 } 203 204 /** 205 * Returns the preference associated with this controller. This may be used in any of the 206 * lifecycle methods, as the preference is set before they are called.. 207 */ getPreference()208 protected final V getPreference() { 209 return mPreference; 210 } 211 212 /** 213 * Called by {@link SettingsFragment} to associate the controller with its preference after the 214 * screen is created. This is guaranteed to be called before {@link #onCreateInternal()}. 215 * 216 * @throws IllegalArgumentException if the given preference does not match the type 217 * returned by {@link #getPreferenceType()} 218 * @throws IllegalStateException if subclass defined initialization is not 219 * complete. 220 */ setPreference(Preference preference)221 final void setPreference(Preference preference) { 222 PreferenceUtil.requirePreferenceType(preference, getPreferenceType()); 223 mPreference = getPreferenceType().cast(preference); 224 mPreference.setOnPreferenceChangeListener( 225 (changedPref, newValue) -> handlePreferenceChanged( 226 getPreferenceType().cast(changedPref), newValue)); 227 mPreference.setOnPreferenceClickListener( 228 clickedPref -> handlePreferenceClicked(getPreferenceType().cast(clickedPref))); 229 checkInitialized(); 230 } 231 232 /** 233 * Called by {@link SettingsFragment} to notify that the applied ux restrictions have changed. 234 * The controller will refresh its UI accordingly unless it is not yet created. In that case, 235 * the UI will refresh once created. 236 */ 237 @Override onUxRestrictionsChanged(CarUxRestrictions uxRestrictions)238 public final void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) { 239 mUxRestrictions = uxRestrictions; 240 refreshUi(); 241 } 242 243 /** 244 * Updates the preference presentation based on its {@link #getAvailabilityStatus()} status. If 245 * the controller is available, the associated preference is shown and a call to {@link 246 * #updateState(Preference)} and {@link #onApplyUxRestrictions(CarUxRestrictions)} are 247 * dispatched to allow the controller to modify the presentation for the current state. If the 248 * controller is not available, the associated preference is hidden from the screen. This is a 249 * no-op if the controller is not yet created. 250 */ refreshUi()251 public final void refreshUi() { 252 if (!mIsCreated) { 253 return; 254 } 255 256 if (isAvailable()) { 257 mPreference.setVisible(true); 258 mPreference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING); 259 updateState(mPreference); 260 onApplyUxRestrictions(mUxRestrictions); 261 } else { 262 mPreference.setVisible(false); 263 } 264 } 265 isAvailable()266 private boolean isAvailable() { 267 int availabilityStatus = getAvailabilityStatus(); 268 return availabilityStatus == AVAILABLE || availabilityStatus == AVAILABLE_FOR_VIEWING; 269 } 270 271 // Controller lifecycle ======================================================================== 272 273 /** 274 * Dispatches a call to {@link #onCreateInternal()} and {@link #refreshUi()} to enable 275 * controllers to setup initial state before a preference is visible. If the controller is 276 * {@link #UNSUPPORTED_ON_DEVICE}, the preference is hidden and no further action is taken. 277 */ 278 @Override onCreate(@onNull LifecycleOwner owner)279 public final void onCreate(@NonNull LifecycleOwner owner) { 280 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 281 mPreference.setVisible(false); 282 return; 283 } 284 onCreateInternal(); 285 mIsCreated = true; 286 refreshUi(); 287 } 288 289 /** 290 * Dispatches a call to {@link #onStartInternal()} and {@link #refreshUi()} to account for any 291 * state changes that may have occurred while the controller was stopped. Returns immediately 292 * if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 293 */ 294 @Override onStart(@onNull LifecycleOwner owner)295 public final void onStart(@NonNull LifecycleOwner owner) { 296 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 297 return; 298 } 299 onStartInternal(); 300 mIsStarted = true; 301 refreshUi(); 302 } 303 304 /** 305 * Notifies that the controller is resumed by dispatching a call to {@link #onResumeInternal()}. 306 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 307 */ 308 @Override onResume(@onNull LifecycleOwner owner)309 public final void onResume(@NonNull LifecycleOwner owner) { 310 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 311 return; 312 } 313 onResumeInternal(); 314 } 315 316 /** 317 * Notifies that the controller is paused by dispatching a call to {@link #onPauseInternal()}. 318 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 319 */ 320 @Override onPause(@onNull LifecycleOwner owner)321 public final void onPause(@NonNull LifecycleOwner owner) { 322 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 323 return; 324 } 325 onPauseInternal(); 326 } 327 328 /** 329 * Notifies that the controller is stopped by dispatching a call to {@link #onStopInternal()}. 330 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 331 */ 332 @Override onStop(@onNull LifecycleOwner owner)333 public final void onStop(@NonNull LifecycleOwner owner) { 334 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 335 return; 336 } 337 mIsStarted = false; 338 onStopInternal(); 339 } 340 341 /** 342 * Notifies that the controller is destroyed by dispatching a call to {@link 343 * #onDestroyInternal()}. Returns immediately if the controller is 344 * {@link #UNSUPPORTED_ON_DEVICE}. 345 */ 346 @Override onDestroy(@onNull LifecycleOwner owner)347 public final void onDestroy(@NonNull LifecycleOwner owner) { 348 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 349 return; 350 } 351 mIsCreated = false; 352 onDestroyInternal(); 353 } 354 355 // Methods for override ======================================================================== 356 357 /** 358 * Returns the upper bound type of the preference on which this controller will operate. 359 */ getPreferenceType()360 protected abstract Class<V> getPreferenceType(); 361 362 /** 363 * Subclasses may override this method to throw {@link IllegalStateException} if any expected 364 * post-instantiation setup is not completed using {@link SettingsFragment#use(Class, int)} 365 * prior to associating the controller with its preference. This will be called before the 366 * controller lifecycle begins. 367 */ checkInitialized()368 protected void checkInitialized() { 369 } 370 371 /** 372 * Returns the {@link AvailabilityStatus} for the setting. This status is used to determine 373 * if the setting should be shown, hidden, or disabled. Defaults to {@link #AVAILABLE}. This 374 * will be called before the controller lifecycle begins and on refresh events. 375 */ 376 @AvailabilityStatus getAvailabilityStatus()377 protected int getAvailabilityStatus() { 378 return AVAILABLE; 379 } 380 381 /** 382 * Subclasses may override this method to complete any operations needed at creation time e.g. 383 * loading static configuration. 384 * 385 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 386 */ onCreateInternal()387 protected void onCreateInternal() { 388 } 389 390 /** 391 * Subclasses may override this method to complete any operations needed each time the 392 * controller is started e.g. registering broadcast receivers. 393 * 394 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 395 */ onStartInternal()396 protected void onStartInternal() { 397 } 398 399 /** 400 * Subclasses may override this method to complete any operations needed each time the 401 * controller is resumed. Prefer to use {@link #onStartInternal()} unless absolutely necessary 402 * as controllers may not be resumed in a multi-display scenario. 403 * 404 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 405 */ onResumeInternal()406 protected void onResumeInternal() { 407 } 408 409 /** 410 * Subclasses may override this method to complete any operations needed each time the 411 * controller is paused. Prefer to use {@link #onStartInternal()} unless absolutely necessary 412 * as controllers may not be resumed in a multi-display scenario. 413 * 414 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 415 */ onPauseInternal()416 protected void onPauseInternal() { 417 } 418 419 /** 420 * Subclasses may override this method to complete any operations needed each time the 421 * controller is stopped e.g. unregistering broadcast receivers. 422 * 423 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 424 */ onStopInternal()425 protected void onStopInternal() { 426 } 427 428 /** 429 * Subclasses may override this method to complete any operations needed when the controller is 430 * destroyed e.g. freeing up held resources. 431 * 432 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 433 */ onDestroyInternal()434 protected void onDestroyInternal() { 435 } 436 437 /** 438 * Subclasses may override this method to update the presentation of the preference for the 439 * current system state (summary, switch state, etc). If the preference has dynamic content 440 * (such as preferences added to a group), it may be updated here as well. 441 * 442 * <p>Important: Operations should be idempotent as this may be called multiple times. 443 * 444 * <p>Note: this will only be called when the following are true: 445 * <ul> 446 * <li>{@link #getAvailabilityStatus()} returns {@link #AVAILABLE} 447 * <li>{@link #onCreateInternal()} has completed. 448 * </ul> 449 */ updateState(V preference)450 protected void updateState(V preference) { 451 } 452 453 /** 454 * Updates the preference enabled status given the {@code restrictionInfo}. This will be called 455 * before the controller lifecycle begins and on refresh events. The preference is disabled by 456 * default when {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP} is set in {@code 457 * uxRestrictions}. Subclasses may override this method to modify enabled state based on 458 * additional driving restrictions. 459 */ onApplyUxRestrictions(CarUxRestrictions uxRestrictions)460 protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) { 461 boolean restrict = shouldApplyUxRestrictions(uxRestrictions); 462 463 restrictPreference(mPreference, restrict); 464 } 465 466 /** 467 * Decides whether or not this {@link PreferenceController} should apply {@code uxRestrictions} 468 * based on the type of restrictions currently present, and the value of the {@code 469 * config_always_ignore_ux_restrictions} and 470 * {@code config_ignore_ux_restrictions} config flags. 471 * <p> 472 * It is not expected that subclasses will override this functionality. If they do, it is 473 * important to respect the config flags being consulted here. 474 * 475 * @return true if {@code uxRestrictions} should be applied and false otherwise. 476 */ shouldApplyUxRestrictions(CarUxRestrictions uxRestrictions)477 protected boolean shouldApplyUxRestrictions(CarUxRestrictions uxRestrictions) { 478 return !isUxRestrictionsIgnored(mAlwaysIgnoreUxRestrictions, 479 mPreferencesIgnoringUxRestrictions) 480 && CarUxRestrictionsHelper.isNoSetup(uxRestrictions) 481 && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING; 482 } 483 484 /** 485 * Updates the UxRestricted state and action for a preference. This will also update all child 486 * preferences with the same state and action when {@param preference} is a PreferenceGroup. 487 * 488 * @param preference the preference to update 489 * @param restrict whether or not the preference should be restricted 490 */ restrictPreference(Preference preference, boolean restrict)491 protected void restrictPreference(Preference preference, boolean restrict) { 492 if (preference instanceof UxRestrictablePreference) { 493 UxRestrictablePreference restrictablePreference = (UxRestrictablePreference) preference; 494 restrictablePreference.setUxRestricted(restrict); 495 restrictablePreference.setOnClickWhileRestrictedListener(p -> 496 Toast.makeText(mContext, mRestrictedWhileDrivingMessage, 497 Toast.LENGTH_LONG).show()); 498 } 499 if (preference instanceof PreferenceGroup) { 500 PreferenceGroup preferenceGroup = (PreferenceGroup) preference; 501 for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { 502 restrictPreference(preferenceGroup.getPreference(i), restrict); 503 } 504 } 505 } 506 507 /** 508 * Updates the clickable while disabled state and action for a preference. This will also 509 * update all child preferences with the same state and action when {@param preference} 510 * is a PreferenceGroup. 511 * 512 * @param preference the preference to update 513 * @param clickable whether or not the preference should be clickable when disabled 514 * @param disabledClickAction the action that should be taken when clicked while disabled 515 */ setClickableWhileDisabled(Preference preference, boolean clickable, @Nullable Consumer<Preference> disabledClickAction)516 protected void setClickableWhileDisabled(Preference preference, boolean clickable, 517 @Nullable Consumer<Preference> disabledClickAction) { 518 if (preference instanceof ClickableWhileDisabledPreference) { 519 ClickableWhileDisabledPreference pref = 520 (ClickableWhileDisabledPreference) preference; 521 pref.setClickableWhileDisabled(clickable); 522 pref.setDisabledClickListener(disabledClickAction); 523 } 524 if (preference instanceof PreferenceGroup) { 525 PreferenceGroup preferenceGroup = (PreferenceGroup) preference; 526 for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { 527 setClickableWhileDisabled(preferenceGroup.getPreference(i), clickable, 528 disabledClickAction); 529 } 530 } 531 } 532 533 /** 534 * Called when the associated preference is changed by the user. This is called before the state 535 * of the preference is updated and before the state is persisted. 536 * 537 * @param preference the changed preference. 538 * @param newValue the new value of the preference. 539 * @return {@code true} to update the state of the preference with the new value. Defaults to 540 * {@code true}. 541 */ handlePreferenceChanged(V preference, Object newValue)542 protected boolean handlePreferenceChanged(V preference, Object newValue) { 543 return true; 544 } 545 546 /** 547 * Called when the preference associated with this controller is clicked. Subclasses may 548 * choose to handle the click event. 549 * 550 * @param preference the clicked preference. 551 * @return {@code true} if click is handled and further propagation should cease. Defaults to 552 * {@code false}. 553 */ handlePreferenceClicked(V preference)554 protected boolean handlePreferenceClicked(V preference) { 555 return false; 556 } 557 isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore)558 protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore) { 559 return allIgnores || prefsThatIgnore.contains(mPreferenceKey); 560 } 561 isStarted()562 protected final boolean isStarted() { 563 return mIsStarted; 564 } 565 } 566