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