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