• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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