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