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