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