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