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_USER, 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_USER} 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 user. 123 */ 124 public static final int DISABLED_FOR_USER = 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 152 /** 153 * Controllers should be instantiated from XML. To pass additional arguments see 154 * {@link SettingsFragment#use(Class, int)}. 155 */ PreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)156 public PreferenceController(Context context, String preferenceKey, 157 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 158 mContext = context; 159 mPreferenceKey = preferenceKey; 160 mFragmentController = fragmentController; 161 mUxRestrictions = uxRestrictions; 162 mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList( 163 mContext.getResources().getStringArray(R.array.config_ignore_ux_restrictions))); 164 mAlwaysIgnoreUxRestrictions = 165 mContext.getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions); 166 mRestrictedWhileDrivingMessage = 167 mContext.getResources().getString(R.string.car_ui_restricted_while_driving); 168 } 169 170 /** 171 * Returns the context used to construct the controller. 172 */ getContext()173 protected final Context getContext() { 174 return mContext; 175 } 176 177 /** 178 * Returns the key for the preference managed by this controller set at construction. 179 */ getPreferenceKey()180 protected final String getPreferenceKey() { 181 return mPreferenceKey; 182 } 183 184 /** 185 * Returns the {@link FragmentController} used to launch fragments and go back to previous 186 * fragments. This is set at construction. 187 */ getFragmentController()188 protected final FragmentController getFragmentController() { 189 return mFragmentController; 190 } 191 192 /** 193 * Returns the current {@link CarUxRestrictions} applied to the controller. Subclasses may use 194 * this to limit which content is displayed in the associated preference. May be called anytime. 195 */ getUxRestrictions()196 protected final CarUxRestrictions getUxRestrictions() { 197 return mUxRestrictions; 198 } 199 200 /** 201 * Returns the preference associated with this controller. This may be used in any of the 202 * lifecycle methods, as the preference is set before they are called.. 203 */ getPreference()204 protected final V getPreference() { 205 return mPreference; 206 } 207 208 /** 209 * Called by {@link SettingsFragment} to associate the controller with its preference after the 210 * screen is created. This is guaranteed to be called before {@link #onCreateInternal()}. 211 * 212 * @throws IllegalArgumentException if the given preference does not match the type 213 * returned by {@link #getPreferenceType()} 214 * @throws IllegalStateException if subclass defined initialization is not 215 * complete. 216 */ setPreference(Preference preference)217 final void setPreference(Preference preference) { 218 PreferenceUtil.requirePreferenceType(preference, getPreferenceType()); 219 mPreference = getPreferenceType().cast(preference); 220 mPreference.setOnPreferenceChangeListener( 221 (changedPref, newValue) -> handlePreferenceChanged( 222 getPreferenceType().cast(changedPref), newValue)); 223 mPreference.setOnPreferenceClickListener( 224 clickedPref -> handlePreferenceClicked(getPreferenceType().cast(clickedPref))); 225 checkInitialized(); 226 } 227 228 /** 229 * Called by {@link SettingsFragment} to notify that the applied ux restrictions have changed. 230 * The controller will refresh its UI accordingly unless it is not yet created. In that case, 231 * the UI will refresh once created. 232 */ 233 @Override onUxRestrictionsChanged(CarUxRestrictions uxRestrictions)234 public final void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) { 235 mUxRestrictions = uxRestrictions; 236 refreshUi(); 237 } 238 239 /** 240 * Updates the preference presentation based on its {@link #getAvailabilityStatus()} status. If 241 * the controller is available, the associated preference is shown and a call to {@link 242 * #updateState(Preference)} and {@link #onApplyUxRestrictions(CarUxRestrictions)} are 243 * dispatched to allow the controller to modify the presentation for the current state. If the 244 * controller is not available, the associated preference is hidden from the screen. This is a 245 * no-op if the controller is not yet created. 246 */ refreshUi()247 public final void refreshUi() { 248 if (!mIsCreated) { 249 return; 250 } 251 252 if (isAvailable()) { 253 mPreference.setVisible(true); 254 mPreference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING); 255 updateState(mPreference); 256 onApplyUxRestrictions(mUxRestrictions); 257 } else { 258 mPreference.setVisible(false); 259 } 260 } 261 isAvailable()262 private boolean isAvailable() { 263 int availabilityStatus = getAvailabilityStatus(); 264 return availabilityStatus == AVAILABLE || availabilityStatus == AVAILABLE_FOR_VIEWING; 265 } 266 267 // Controller lifecycle ======================================================================== 268 269 /** 270 * Dispatches a call to {@link #onCreateInternal()} and {@link #refreshUi()} to enable 271 * controllers to setup initial state before a preference is visible. If the controller is 272 * {@link #UNSUPPORTED_ON_DEVICE}, the preference is hidden and no further action is taken. 273 */ 274 @Override onCreate(@onNull LifecycleOwner owner)275 public final void onCreate(@NonNull LifecycleOwner owner) { 276 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 277 mPreference.setVisible(false); 278 return; 279 } 280 onCreateInternal(); 281 mIsCreated = true; 282 refreshUi(); 283 } 284 285 /** 286 * Dispatches a call to {@link #onStartInternal()} and {@link #refreshUi()} to account for any 287 * state changes that may have occurred while the controller was stopped. Returns immediately 288 * if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 289 */ 290 @Override onStart(@onNull LifecycleOwner owner)291 public final void onStart(@NonNull LifecycleOwner owner) { 292 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 293 return; 294 } 295 onStartInternal(); 296 refreshUi(); 297 } 298 299 /** 300 * Notifies that the controller is resumed by dispatching a call to {@link #onResumeInternal()}. 301 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 302 */ 303 @Override onResume(@onNull LifecycleOwner owner)304 public final void onResume(@NonNull LifecycleOwner owner) { 305 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 306 return; 307 } 308 onResumeInternal(); 309 } 310 311 /** 312 * Notifies that the controller is paused by dispatching a call to {@link #onPauseInternal()}. 313 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 314 */ 315 @Override onPause(@onNull LifecycleOwner owner)316 public final void onPause(@NonNull LifecycleOwner owner) { 317 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 318 return; 319 } 320 onPauseInternal(); 321 } 322 323 /** 324 * Notifies that the controller is stopped by dispatching a call to {@link #onStopInternal()}. 325 * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}. 326 */ 327 @Override onStop(@onNull LifecycleOwner owner)328 public final void onStop(@NonNull LifecycleOwner owner) { 329 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 330 return; 331 } 332 onStopInternal(); 333 } 334 335 /** 336 * Notifies that the controller is destroyed by dispatching a call to {@link 337 * #onDestroyInternal()}. Returns immediately if the controller is 338 * {@link #UNSUPPORTED_ON_DEVICE}. 339 */ 340 @Override onDestroy(@onNull LifecycleOwner owner)341 public final void onDestroy(@NonNull LifecycleOwner owner) { 342 if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) { 343 return; 344 } 345 mIsCreated = false; 346 onDestroyInternal(); 347 } 348 349 // Methods for override ======================================================================== 350 351 /** 352 * Returns the upper bound type of the preference on which this controller will operate. 353 */ getPreferenceType()354 protected abstract Class<V> getPreferenceType(); 355 356 /** 357 * Subclasses may override this method to throw {@link IllegalStateException} if any expected 358 * post-instantiation setup is not completed using {@link SettingsFragment#use(Class, int)} 359 * prior to associating the controller with its preference. This will be called before the 360 * controller lifecycle begins. 361 */ checkInitialized()362 protected void checkInitialized() { 363 } 364 365 /** 366 * Returns the {@link AvailabilityStatus} for the setting. This status is used to determine 367 * if the setting should be shown, hidden, or disabled. Defaults to {@link #AVAILABLE}. This 368 * will be called before the controller lifecycle begins and on refresh events. 369 */ 370 @AvailabilityStatus getAvailabilityStatus()371 protected int getAvailabilityStatus() { 372 return AVAILABLE; 373 } 374 375 /** 376 * Subclasses may override this method to complete any operations needed at creation time e.g. 377 * loading static configuration. 378 * 379 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 380 */ onCreateInternal()381 protected void onCreateInternal() { 382 } 383 384 /** 385 * Subclasses may override this method to complete any operations needed each time the 386 * controller is started e.g. registering broadcast receivers. 387 * 388 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 389 */ onStartInternal()390 protected void onStartInternal() { 391 } 392 393 /** 394 * Subclasses may override this method to complete any operations needed each time the 395 * controller is resumed. Prefer to use {@link #onStartInternal()} unless absolutely necessary 396 * as controllers may not be resumed in a multi-display scenario. 397 * 398 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 399 */ onResumeInternal()400 protected void onResumeInternal() { 401 } 402 403 /** 404 * Subclasses may override this method to complete any operations needed each time the 405 * controller is paused. Prefer to use {@link #onStartInternal()} unless absolutely necessary 406 * as controllers may not be resumed in a multi-display scenario. 407 * 408 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 409 */ onPauseInternal()410 protected void onPauseInternal() { 411 } 412 413 /** 414 * Subclasses may override this method to complete any operations needed each time the 415 * controller is stopped e.g. unregistering broadcast receivers. 416 * 417 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 418 */ onStopInternal()419 protected void onStopInternal() { 420 } 421 422 /** 423 * Subclasses may override this method to complete any operations needed when the controller is 424 * destroyed e.g. freeing up held resources. 425 * 426 * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers. 427 */ onDestroyInternal()428 protected void onDestroyInternal() { 429 } 430 431 /** 432 * Subclasses may override this method to update the presentation of the preference for the 433 * current system state (summary, switch state, etc). If the preference has dynamic content 434 * (such as preferences added to a group), it may be updated here as well. 435 * 436 * <p>Important: Operations should be idempotent as this may be called multiple times. 437 * 438 * <p>Note: this will only be called when the following are true: 439 * <ul> 440 * <li>{@link #getAvailabilityStatus()} returns {@link #AVAILABLE} 441 * <li>{@link #onCreateInternal()} has completed. 442 * </ul> 443 */ updateState(V preference)444 protected void updateState(V preference) { 445 } 446 447 /** 448 * Updates the preference enabled status given the {@code restrictionInfo}. This will be called 449 * before the controller lifecycle begins and on refresh events. The preference is disabled by 450 * default when {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP} is set in {@code 451 * uxRestrictions}. Subclasses may override this method to modify enabled state based on 452 * additional driving restrictions. 453 */ onApplyUxRestrictions(CarUxRestrictions uxRestrictions)454 protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) { 455 boolean restrict = false; 456 if (!isUxRestrictionsIgnored(mAlwaysIgnoreUxRestrictions, 457 mPreferencesIgnoringUxRestrictions) 458 && CarUxRestrictionsHelper.isNoSetup(uxRestrictions) 459 && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING) { 460 restrict = true; 461 } 462 restrictPreference(mPreference, restrict); 463 } 464 465 /** 466 * Updates the UxRestricted state and action for a preference. This will also update all child 467 * preferences with the same state and action when {@param preference} is a PreferenceGroup. 468 * 469 * @param preference the preference to update 470 * @param restrict whether or not the preference should be restricted 471 */ restrictPreference(Preference preference, boolean restrict)472 protected void restrictPreference(Preference preference, boolean restrict) { 473 if (preference instanceof UxRestrictablePreference) { 474 UxRestrictablePreference restrictablePreference = (UxRestrictablePreference) preference; 475 restrictablePreference.setUxRestricted(restrict); 476 restrictablePreference.setOnClickWhileRestrictedListener(p -> 477 Toast.makeText(mContext, mRestrictedWhileDrivingMessage, 478 Toast.LENGTH_LONG).show()); 479 } 480 if (preference instanceof PreferenceGroup) { 481 PreferenceGroup preferenceGroup = (PreferenceGroup) preference; 482 for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { 483 restrictPreference(preferenceGroup.getPreference(i), restrict); 484 } 485 } 486 } 487 488 /** 489 * Called when the associated preference is changed by the user. This is called before the state 490 * of the preference is updated and before the state is persisted. 491 * 492 * @param preference the changed preference. 493 * @param newValue the new value of the preference. 494 * @return {@code true} to update the state of the preference with the new value. Defaults to 495 * {@code true}. 496 */ handlePreferenceChanged(V preference, Object newValue)497 protected boolean handlePreferenceChanged(V preference, Object newValue) { 498 return true; 499 } 500 501 /** 502 * Called when the preference associated with this controller is clicked. Subclasses may 503 * choose to handle the click event. 504 * 505 * @param preference the clicked preference. 506 * @return {@code true} if click is handled and further propagation should cease. Defaults to 507 * {@code false}. 508 */ handlePreferenceClicked(V preference)509 protected boolean handlePreferenceClicked(V preference) { 510 return false; 511 } 512 isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore)513 protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore) { 514 return allIgnores || prefsThatIgnore.contains(mPreferenceKey); 515 } 516 } 517