• 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_USER,
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_USER} 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 user.
123      */
124     public static final int DISABLED_FOR_USER = 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 
152     /**
153      * Controllers should be instantiated from XML. To pass additional arguments see
154      * {@link SettingsFragment#use(Class, int)}.
155      */
PreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)156     public PreferenceController(Context context, String preferenceKey,
157             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
158         mContext = context;
159         mPreferenceKey = preferenceKey;
160         mFragmentController = fragmentController;
161         mUxRestrictions = uxRestrictions;
162         mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList(
163                 mContext.getResources().getStringArray(R.array.config_ignore_ux_restrictions)));
164         mAlwaysIgnoreUxRestrictions =
165                 mContext.getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions);
166         mRestrictedWhileDrivingMessage =
167                 mContext.getResources().getString(R.string.car_ui_restricted_while_driving);
168     }
169 
170     /**
171      * Returns the context used to construct the controller.
172      */
getContext()173     protected final Context getContext() {
174         return mContext;
175     }
176 
177     /**
178      * Returns the key for the preference managed by this controller set at construction.
179      */
getPreferenceKey()180     protected final String getPreferenceKey() {
181         return mPreferenceKey;
182     }
183 
184     /**
185      * Returns the {@link FragmentController} used to launch fragments and go back to previous
186      * fragments. This is set at construction.
187      */
getFragmentController()188     protected final FragmentController getFragmentController() {
189         return mFragmentController;
190     }
191 
192     /**
193      * Returns the current {@link CarUxRestrictions} applied to the controller. Subclasses may use
194      * this to limit which content is displayed in the associated preference. May be called anytime.
195      */
getUxRestrictions()196     protected final CarUxRestrictions getUxRestrictions() {
197         return mUxRestrictions;
198     }
199 
200     /**
201      * Returns the preference associated with this controller. This may be used in any of the
202      * lifecycle methods, as the preference is set before they are called..
203      */
getPreference()204     protected final V getPreference() {
205         return mPreference;
206     }
207 
208     /**
209      * Called by {@link SettingsFragment} to associate the controller with its preference after the
210      * screen is created. This is guaranteed to be called before {@link #onCreateInternal()}.
211      *
212      * @throws IllegalArgumentException if the given preference does not match the type
213      *                                  returned by {@link #getPreferenceType()}
214      * @throws IllegalStateException    if subclass defined initialization is not
215      *                                  complete.
216      */
setPreference(Preference preference)217     final void setPreference(Preference preference) {
218         PreferenceUtil.requirePreferenceType(preference, getPreferenceType());
219         mPreference = getPreferenceType().cast(preference);
220         mPreference.setOnPreferenceChangeListener(
221                 (changedPref, newValue) -> handlePreferenceChanged(
222                         getPreferenceType().cast(changedPref), newValue));
223         mPreference.setOnPreferenceClickListener(
224                 clickedPref -> handlePreferenceClicked(getPreferenceType().cast(clickedPref)));
225         checkInitialized();
226     }
227 
228     /**
229      * Called by {@link SettingsFragment} to notify that the applied ux restrictions have changed.
230      * The controller will refresh its UI accordingly unless it is not yet created. In that case,
231      * the UI will refresh once created.
232      */
233     @Override
onUxRestrictionsChanged(CarUxRestrictions uxRestrictions)234     public final void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) {
235         mUxRestrictions = uxRestrictions;
236         refreshUi();
237     }
238 
239     /**
240      * Updates the preference presentation based on its {@link #getAvailabilityStatus()} status. If
241      * the controller is available, the associated preference is shown and a call to {@link
242      * #updateState(Preference)} and {@link #onApplyUxRestrictions(CarUxRestrictions)} are
243      * dispatched to allow the controller to modify the presentation for the current state. If the
244      * controller is not available, the associated preference is hidden from the screen. This is a
245      * no-op if the controller is not yet created.
246      */
refreshUi()247     public final void refreshUi() {
248         if (!mIsCreated) {
249             return;
250         }
251 
252         if (isAvailable()) {
253             mPreference.setVisible(true);
254             mPreference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING);
255             updateState(mPreference);
256             onApplyUxRestrictions(mUxRestrictions);
257         } else {
258             mPreference.setVisible(false);
259         }
260     }
261 
isAvailable()262     private boolean isAvailable() {
263         int availabilityStatus = getAvailabilityStatus();
264         return availabilityStatus == AVAILABLE || availabilityStatus == AVAILABLE_FOR_VIEWING;
265     }
266 
267     // Controller lifecycle ========================================================================
268 
269     /**
270      * Dispatches a call to {@link #onCreateInternal()} and {@link #refreshUi()} to enable
271      * controllers to setup initial state before a preference is visible. If the controller is
272      * {@link #UNSUPPORTED_ON_DEVICE}, the preference is hidden and no further action is taken.
273      */
274     @Override
onCreate(@onNull LifecycleOwner owner)275     public final void onCreate(@NonNull LifecycleOwner owner) {
276         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
277             mPreference.setVisible(false);
278             return;
279         }
280         onCreateInternal();
281         mIsCreated = true;
282         refreshUi();
283     }
284 
285     /**
286      * Dispatches a call to {@link #onStartInternal()} and {@link #refreshUi()} to account for any
287      * state changes that may have occurred while the controller was stopped. Returns immediately
288      * if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
289      */
290     @Override
onStart(@onNull LifecycleOwner owner)291     public final void onStart(@NonNull LifecycleOwner owner) {
292         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
293             return;
294         }
295         onStartInternal();
296         refreshUi();
297     }
298 
299     /**
300      * Notifies that the controller is resumed by dispatching a call to {@link #onResumeInternal()}.
301      * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
302      */
303     @Override
onResume(@onNull LifecycleOwner owner)304     public final void onResume(@NonNull LifecycleOwner owner) {
305         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
306             return;
307         }
308         onResumeInternal();
309     }
310 
311     /**
312      * Notifies that the controller is paused by dispatching a call to {@link #onPauseInternal()}.
313      * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
314      */
315     @Override
onPause(@onNull LifecycleOwner owner)316     public final void onPause(@NonNull LifecycleOwner owner) {
317         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
318             return;
319         }
320         onPauseInternal();
321     }
322 
323     /**
324      * Notifies that the controller is stopped by dispatching a call to {@link #onStopInternal()}.
325      * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
326      */
327     @Override
onStop(@onNull LifecycleOwner owner)328     public final void onStop(@NonNull LifecycleOwner owner) {
329         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
330             return;
331         }
332         onStopInternal();
333     }
334 
335     /**
336      * Notifies that the controller is destroyed by dispatching a call to {@link
337      * #onDestroyInternal()}. Returns immediately if the controller is
338      * {@link #UNSUPPORTED_ON_DEVICE}.
339      */
340     @Override
onDestroy(@onNull LifecycleOwner owner)341     public final void onDestroy(@NonNull LifecycleOwner owner) {
342         if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
343             return;
344         }
345         mIsCreated = false;
346         onDestroyInternal();
347     }
348 
349     // Methods for override ========================================================================
350 
351     /**
352      * Returns the upper bound type of the preference on which this controller will operate.
353      */
getPreferenceType()354     protected abstract Class<V> getPreferenceType();
355 
356     /**
357      * Subclasses may override this method to throw {@link IllegalStateException} if any expected
358      * post-instantiation setup is not completed using {@link SettingsFragment#use(Class, int)}
359      * prior to associating the controller with its preference. This will be called before the
360      * controller lifecycle begins.
361      */
checkInitialized()362     protected void checkInitialized() {
363     }
364 
365     /**
366      * Returns the {@link AvailabilityStatus} for the setting. This status is used to determine
367      * if the setting should be shown, hidden, or disabled. Defaults to {@link #AVAILABLE}. This
368      * will be called before the controller lifecycle begins and on refresh events.
369      */
370     @AvailabilityStatus
getAvailabilityStatus()371     protected int getAvailabilityStatus() {
372         return AVAILABLE;
373     }
374 
375     /**
376      * Subclasses may override this method to complete any operations needed at creation time e.g.
377      * loading static configuration.
378      *
379      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
380      */
onCreateInternal()381     protected void onCreateInternal() {
382     }
383 
384     /**
385      * Subclasses may override this method to complete any operations needed each time the
386      * controller is started e.g. registering broadcast receivers.
387      *
388      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
389      */
onStartInternal()390     protected void onStartInternal() {
391     }
392 
393     /**
394      * Subclasses may override this method to complete any operations needed each time the
395      * controller is resumed. Prefer to use {@link #onStartInternal()} unless absolutely necessary
396      * as controllers may not be resumed in a multi-display scenario.
397      *
398      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
399      */
onResumeInternal()400     protected void onResumeInternal() {
401     }
402 
403     /**
404      * Subclasses may override this method to complete any operations needed each time the
405      * controller is paused. Prefer to use {@link #onStartInternal()} unless absolutely necessary
406      * as controllers may not be resumed in a multi-display scenario.
407      *
408      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
409      */
onPauseInternal()410     protected void onPauseInternal() {
411     }
412 
413     /**
414      * Subclasses may override this method to complete any operations needed each time the
415      * controller is stopped e.g. unregistering broadcast receivers.
416      *
417      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
418      */
onStopInternal()419     protected void onStopInternal() {
420     }
421 
422     /**
423      * Subclasses may override this method to complete any operations needed when the controller is
424      * destroyed e.g. freeing up held resources.
425      *
426      * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
427      */
onDestroyInternal()428     protected void onDestroyInternal() {
429     }
430 
431     /**
432      * Subclasses may override this method to update the presentation of the preference for the
433      * current system state (summary, switch state, etc). If the preference has dynamic content
434      * (such as preferences added to a group), it may be updated here as well.
435      *
436      * <p>Important: Operations should be idempotent as this may be called multiple times.
437      *
438      * <p>Note: this will only be called when the following are true:
439      * <ul>
440      * <li>{@link #getAvailabilityStatus()} returns {@link #AVAILABLE}
441      * <li>{@link #onCreateInternal()} has completed.
442      * </ul>
443      */
updateState(V preference)444     protected void updateState(V preference) {
445     }
446 
447     /**
448      * Updates the preference enabled status given the {@code restrictionInfo}. This will be called
449      * before the controller lifecycle begins and on refresh events. The preference is disabled by
450      * default when {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP} is set in {@code
451      * uxRestrictions}. Subclasses may override this method to modify enabled state based on
452      * additional driving restrictions.
453      */
onApplyUxRestrictions(CarUxRestrictions uxRestrictions)454     protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
455         boolean restrict = false;
456         if (!isUxRestrictionsIgnored(mAlwaysIgnoreUxRestrictions,
457                 mPreferencesIgnoringUxRestrictions)
458                 && CarUxRestrictionsHelper.isNoSetup(uxRestrictions)
459                 && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING) {
460             restrict = true;
461         }
462         restrictPreference(mPreference, restrict);
463     }
464 
465     /**
466      * Updates the UxRestricted state and action for a preference. This will also update all child
467      * preferences with the same state and action when {@param preference} is a PreferenceGroup.
468      *
469      * @param preference the preference to update
470      * @param restrict whether or not the preference should be restricted
471      */
restrictPreference(Preference preference, boolean restrict)472     protected void restrictPreference(Preference preference, boolean restrict) {
473         if (preference instanceof UxRestrictablePreference) {
474             UxRestrictablePreference restrictablePreference = (UxRestrictablePreference) preference;
475             restrictablePreference.setUxRestricted(restrict);
476             restrictablePreference.setOnClickWhileRestrictedListener(p ->
477                     Toast.makeText(mContext, mRestrictedWhileDrivingMessage,
478                             Toast.LENGTH_LONG).show());
479         }
480         if (preference instanceof PreferenceGroup) {
481             PreferenceGroup preferenceGroup = (PreferenceGroup) preference;
482             for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
483                 restrictPreference(preferenceGroup.getPreference(i), restrict);
484             }
485         }
486     }
487 
488     /**
489      * Called when the associated preference is changed by the user. This is called before the state
490      * of the preference is updated and before the state is persisted.
491      *
492      * @param preference the changed preference.
493      * @param newValue   the new value of the preference.
494      * @return {@code true} to update the state of the preference with the new value. Defaults to
495      * {@code true}.
496      */
handlePreferenceChanged(V preference, Object newValue)497     protected boolean handlePreferenceChanged(V preference, Object newValue) {
498         return true;
499     }
500 
501     /**
502      * Called when the preference associated with this controller is clicked. Subclasses may
503      * choose to handle the click event.
504      *
505      * @param preference the clicked preference.
506      * @return {@code true} if click is handled and further propagation should cease. Defaults to
507      * {@code false}.
508      */
handlePreferenceClicked(V preference)509     protected boolean handlePreferenceClicked(V preference) {
510         return false;
511     }
512 
isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore)513     protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore) {
514         return allIgnores || prefsThatIgnore.contains(mPreferenceKey);
515     }
516 }
517