• 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 static com.android.car.settings.common.BaseCarSettingsActivity.META_DATA_KEY_SINGLE_PANE;
20 
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.car.drivingstate.CarUxRestrictionsManager;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentSender;
26 import android.os.Bundle;
27 import android.util.ArrayMap;
28 import android.util.SparseArray;
29 import android.util.TypedValue;
30 import android.view.ContextThemeWrapper;
31 import android.view.LayoutInflater;
32 import android.view.ViewGroup;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.StringRes;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.annotation.XmlRes;
39 import androidx.fragment.app.DialogFragment;
40 import androidx.fragment.app.Fragment;
41 import androidx.lifecycle.Lifecycle;
42 import androidx.preference.Preference;
43 import androidx.preference.PreferenceScreen;
44 import androidx.recyclerview.widget.RecyclerView;
45 
46 import com.android.car.settings.R;
47 import com.android.car.ui.baselayout.Insets;
48 import com.android.car.ui.preference.PreferenceFragment;
49 import com.android.car.ui.recyclerview.CarUiRecyclerView;
50 import com.android.car.ui.toolbar.MenuItem;
51 import com.android.car.ui.toolbar.NavButtonMode;
52 import com.android.car.ui.toolbar.ToolbarController;
53 import com.android.car.ui.utils.ViewUtils;
54 import com.android.settingslib.search.Indexable;
55 
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Map;
59 
60 /**
61  * Base fragment for all settings. Subclasses must provide a resource id via
62  * {@link #getPreferenceScreenResId()} for the XML resource which defines the preferences to
63  * display and controllers to update their state. This class is responsible for displaying the
64  * preferences, creating {@link PreferenceController} instances from the metadata, and
65  * associating the preferences with their corresponding controllers.
66  *
67  * <p>{@code preferenceTheme} must be specified in the application theme, and the parent to which
68  * this fragment attaches must implement {@link UxRestrictionsProvider} and
69  * {@link FragmentController} or an {@link IllegalStateException} will be thrown during
70  * {@link #onAttach(Context)}. Changes to driving state restrictions are propagated to
71  * controllers.
72  */
73 public abstract class SettingsFragment extends PreferenceFragment implements
74         CarUxRestrictionsManager.OnUxRestrictionsChangedListener, FragmentController, Indexable {
75 
76     @VisibleForTesting
77     static final String DIALOG_FRAGMENT_TAG =
78             "com.android.car.settings.common.SettingsFragment.DIALOG";
79 
80     private static final int MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS = 0xff - 1;
81 
82     private final Map<Class, List<PreferenceController>> mPreferenceControllersLookup =
83             new ArrayMap<>();
84     private final List<PreferenceController> mPreferenceControllers = new ArrayList<>();
85     private final SparseArray<ActivityResultCallback> mActivityResultCallbackMap =
86             new SparseArray<>();
87 
88     private CarUxRestrictions mUxRestrictions;
89     private HighlightablePreferenceGroupAdapter mAdapter;
90     private int mCurrentRequestIndex = 0;
91 
92     /**
93      * Returns the resource id for the preference XML of this fragment.
94      */
95     @XmlRes
getPreferenceScreenResId()96     protected abstract int getPreferenceScreenResId();
97 
getToolbar()98     protected ToolbarController getToolbar() {
99         return getFragmentHost().getToolbar();
100     }
101     /**
102      * Returns the MenuItems to display in the toolbar. Subclasses should override this to
103      * add additional buttons, switches, ect. to the toolbar.
104      */
getToolbarMenuItems()105     protected List<MenuItem> getToolbarMenuItems() {
106         return null;
107     }
108 
109     /**
110      * Returns the controller of the given {@code clazz} for the given {@code
111      * preferenceKeyResId}. Subclasses may use this method in {@link #onAttach(Context)} to call
112      * setters on controllers to pass additional arguments after construction.
113      *
114      * <p>For example:
115      * <pre>{@code
116      * @Override
117      * public void onAttach(Context context) {
118      *     super.onAttach(context);
119      *     use(MyPreferenceController.class, R.string.pk_my_key).setMyArg(myArg);
120      * }
121      * }</pre>
122      *
123      * <p>Important: Use judiciously to minimize tight coupling between controllers and fragments.
124      */
125     @SuppressWarnings("unchecked") // Class is used as map key.
use(Class<T> clazz, @StringRes int preferenceKeyResId)126     protected <T extends PreferenceController> T use(Class<T> clazz,
127             @StringRes int preferenceKeyResId) {
128         List<PreferenceController> controllerList = mPreferenceControllersLookup.get(clazz);
129         if (controllerList != null) {
130             String preferenceKey = getString(preferenceKeyResId);
131             for (PreferenceController controller : controllerList) {
132                 if (controller.getPreferenceKey().equals(preferenceKey)) {
133                     return (T) controller;
134                 }
135             }
136         }
137         return null;
138     }
139 
140     /**
141      * Enables rotary scrolling for the {@link CarUiRecyclerView} in this fragment.
142      * <p>
143      * Rotary scrolling should be enabled for scrolling views which contain content which the user
144      * may want to see but can't interact with, either alone or along with interactive (focusable)
145      * content.
146      */
enableRotaryScroll()147     protected void enableRotaryScroll() {
148         CarUiRecyclerView recyclerView = getView().findViewById(R.id.settings_recycler_view);
149         if (recyclerView != null) {
150             ViewUtils.setRotaryScrollEnabled(recyclerView, /* isVertical= */ true);
151         }
152     }
153 
154     @Override
onAttach(Context context)155     public void onAttach(Context context) {
156         super.onAttach(context);
157         if (!(getActivity() instanceof UxRestrictionsProvider)) {
158             throw new IllegalStateException("Must attach to a UxRestrictionsProvider");
159         }
160         if (!(getActivity() instanceof FragmentHost)) {
161             throw new IllegalStateException("Must attach to a FragmentHost");
162         }
163 
164         TypedValue tv = new TypedValue();
165         getActivity().getTheme().resolveAttribute(androidx.preference.R.attr.preferenceTheme, tv,
166                 true);
167         int theme = tv.resourceId;
168         if (theme == 0) {
169             throw new IllegalStateException("Must specify preferenceTheme in theme");
170         }
171         // Construct a context with the theme as controllers may create new preferences.
172         Context styledContext = new ContextThemeWrapper(getActivity(), theme);
173 
174         mUxRestrictions = ((UxRestrictionsProvider) requireActivity()).getCarUxRestrictions();
175         mPreferenceControllers.clear();
176         mPreferenceControllers.addAll(
177                 PreferenceControllerListHelper.getPreferenceControllersFromXml(styledContext,
178                         getPreferenceScreenResId(), /* fragmentController= */ this,
179                         mUxRestrictions));
180 
181         Lifecycle lifecycle = getLifecycle();
182         mPreferenceControllers.forEach(controller -> {
183             lifecycle.addObserver(controller);
184             mPreferenceControllersLookup.computeIfAbsent(controller.getClass(),
185                     k -> new ArrayList<>(/* initialCapacity= */ 1)).add(controller);
186         });
187     }
188 
189     /**
190      * Inflates the preferences from {@link #getPreferenceScreenResId()} and associates the
191      * preference with their corresponding {@link PreferenceController} instances.
192      */
193     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)194     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
195         @XmlRes int resId = getPreferenceScreenResId();
196         if (resId <= 0) {
197             throw new IllegalStateException(
198                     "Fragment must specify a preference screen resource ID");
199         }
200         addPreferencesFromResource(resId);
201         PreferenceScreen screen = getPreferenceScreen();
202         for (PreferenceController controller : mPreferenceControllers) {
203             Preference pref = screen.findPreference(controller.getPreferenceKey());
204 
205             controller.setPreference(pref);
206         }
207     }
208 
209     @Override
onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)210     public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
211             Bundle savedInstanceState) {
212         inflater.inflate(R.layout.settings_recyclerview_default, parent, /* attachToRoot= */ true);
213         return parent.requireViewById(R.id.settings_recycler_view);
214     }
215 
216     @Override
setupToolbar(@onNull ToolbarController toolbar)217     protected void setupToolbar(@NonNull ToolbarController toolbar) {
218         List<MenuItem> items = getToolbarMenuItems();
219         if (items != null) {
220             if (items.size() == 1) {
221                 items.get(0).setId(R.id.toolbar_menu_item_0);
222             } else if (items.size() == 2) {
223                 items.get(0).setId(R.id.toolbar_menu_item_0);
224                 items.get(1).setId(R.id.toolbar_menu_item_1);
225             }
226         }
227         toolbar.setTitle(getPreferenceScreen().getTitle());
228         toolbar.setMenuItems(items);
229         toolbar.setLogo(null);
230         if (getActivity().getIntent().getBooleanExtra(META_DATA_KEY_SINGLE_PANE, false)) {
231             toolbar.setNavButtonMode(NavButtonMode.BACK);
232         }
233     }
234 
235     @Override
onDetach()236     public void onDetach() {
237         super.onDetach();
238         Lifecycle lifecycle = getLifecycle();
239         mPreferenceControllers.forEach(lifecycle::removeObserver);
240         mActivityResultCallbackMap.clear();
241     }
242 
243     @Override
onCreateAdapter(PreferenceScreen preferenceScreen)244     protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
245         mAdapter = createHighlightableAdapter(preferenceScreen);
246         return mAdapter;
247     }
248 
249     /**
250      * Returns a HighlightablePreferenceGroupAdapter to be used as the RecyclerView.Adapter for
251      * this fragment. Subclasses can override this method to return their own
252      * HighlightablePreferenceGroupAdapter instance.
253      */
createHighlightableAdapter( PreferenceScreen preferenceScreen)254     protected HighlightablePreferenceGroupAdapter createHighlightableAdapter(
255             PreferenceScreen preferenceScreen) {
256         return new HighlightablePreferenceGroupAdapter(preferenceScreen);
257     }
258 
requestPreferenceHighlight(String key)259     protected void requestPreferenceHighlight(String key) {
260         if (mAdapter != null) {
261             mAdapter.requestHighlight(getView(), getListView(), key);
262         }
263     }
264 
clearPreferenceHighlight()265     protected void clearPreferenceHighlight() {
266         if (mAdapter != null) {
267             mAdapter.clearHighlight(getView());
268         }
269     }
270 
271     /**
272      * Notifies {@link PreferenceController} instances of changes to {@link CarUxRestrictions}.
273      */
274     @Override
onUxRestrictionsChanged(CarUxRestrictions uxRestrictions)275     public void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) {
276         if (!uxRestrictions.isSameRestrictions(mUxRestrictions)) {
277             mUxRestrictions = uxRestrictions;
278             for (PreferenceController controller : mPreferenceControllers) {
279                 controller.onUxRestrictionsChanged(uxRestrictions);
280             }
281         }
282     }
283 
284     /**
285      * {@inheritDoc}
286      *
287      * <p>Settings needs to launch custom dialog types in order to extend the Device Default theme.
288      *
289      * @param preference The Preference object requesting the dialog.
290      */
291     @Override
onDisplayPreferenceDialog(Preference preference)292     public void onDisplayPreferenceDialog(Preference preference) {
293         // check if dialog is already showing
294         if (findDialogByTag(DIALOG_FRAGMENT_TAG) != null) {
295             return;
296         }
297 
298         if (preference instanceof ValidatedEditTextPreference) {
299             DialogFragment dialogFragment = preference instanceof PasswordEditTextPreference
300                     ? PasswordEditTextPreferenceDialogFragment.newInstance(preference.getKey())
301                     : ValidatedEditTextPreferenceDialogFragment.newInstance(preference.getKey());
302 
303             dialogFragment.setTargetFragment(/* fragment= */ this, /* requestCode= */ 0);
304             showDialog(dialogFragment, DIALOG_FRAGMENT_TAG);
305         } else {
306             super.onDisplayPreferenceDialog(preference);
307         }
308     }
309 
310     @Override
launchFragment(Fragment fragment)311     public void launchFragment(Fragment fragment) {
312         getFragmentHost().launchFragment(fragment);
313     }
314 
315     @Override
goBack()316     public void goBack() {
317         getFragmentHost().goBack();
318     }
319 
320     @Override
showDialog(DialogFragment dialogFragment, @Nullable String tag)321     public void showDialog(DialogFragment dialogFragment, @Nullable String tag) {
322         dialogFragment.show(getFragmentManager(), tag);
323     }
324 
325     @Override
showProgressBar(boolean visible)326     public void showProgressBar(boolean visible) {
327         getToolbar().getProgressBar().setVisible(visible);
328     }
329 
330     @Nullable
331     @Override
findDialogByTag(String tag)332     public DialogFragment findDialogByTag(String tag) {
333         Fragment fragment = getFragmentManager().findFragmentByTag(tag);
334         if (fragment instanceof DialogFragment) {
335             return (DialogFragment) fragment;
336         }
337         return null;
338     }
339 
340     @NonNull
341     @Override
getSettingsLifecycle()342     public Lifecycle getSettingsLifecycle() {
343         return getLifecycle();
344     }
345 
346     @Override
startActivityForResult(Intent intent, int requestCode, ActivityResultCallback callback)347     public void startActivityForResult(Intent intent, int requestCode,
348             ActivityResultCallback callback) {
349         validateRequestCodeForPreferenceController(requestCode);
350         int requestIndex = allocateRequestIndex(callback);
351         super.startActivityForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff));
352     }
353 
354     @Override
startIntentSenderForResult(IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, Bundle options, ActivityResultCallback callback)355     public void startIntentSenderForResult(IntentSender intent, int requestCode,
356             @Nullable Intent fillInIntent, int flagsMask, int flagsValues, Bundle options,
357             ActivityResultCallback callback)
358             throws IntentSender.SendIntentException {
359         validateRequestCodeForPreferenceController(requestCode);
360         int requestIndex = allocateRequestIndex(callback);
361         super.startIntentSenderForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff),
362                 fillInIntent, flagsMask, flagsValues, /* extraFlags= */ 0, options);
363     }
364 
365     @Override
onActivityResult(int requestCode, int resultCode, Intent data)366     public void onActivityResult(int requestCode, int resultCode, Intent data) {
367         super.onActivityResult(requestCode, resultCode, data);
368         int requestIndex = (requestCode >> 8) & 0xff;
369         if (requestIndex != 0) {
370             requestIndex--;
371             ActivityResultCallback callback = mActivityResultCallbackMap.get(requestIndex);
372             mActivityResultCallbackMap.remove(requestIndex);
373             if (callback != null) {
374                 callback.processActivityResult(requestCode & 0xff, resultCode, data);
375             }
376         }
377     }
378 
379     @Override
getPreferenceToolbar(@onNull Fragment fragment)380     protected ToolbarController getPreferenceToolbar(@NonNull Fragment fragment) {
381         return getToolbar();
382     }
383 
384     @Override
getPreferenceInsets(@onNull Fragment fragment)385     protected Insets getPreferenceInsets(@NonNull Fragment fragment) {
386         return null;
387     }
388 
389     // Allocates the next available startActivityForResult request index.
allocateRequestIndex(ActivityResultCallback callback)390     private int allocateRequestIndex(ActivityResultCallback callback) {
391         // Check that we haven't exhausted the request index space.
392         if (mActivityResultCallbackMap.size() >= MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS) {
393             throw new IllegalStateException(
394                     "Too many pending activity result callbacks.");
395         }
396 
397         // Find an unallocated request index in the mPendingFragmentActivityResults map.
398         while (mActivityResultCallbackMap.indexOfKey(mCurrentRequestIndex) >= 0) {
399             mCurrentRequestIndex =
400                     (mCurrentRequestIndex + 1) % MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS;
401         }
402 
403         mActivityResultCallbackMap.put(mCurrentRequestIndex, callback);
404         return mCurrentRequestIndex;
405     }
406 
407     /**
408      * Checks whether the given request code is a valid code by masking it with 0xff00. Throws an
409      * {@link IllegalArgumentException} if the code is not valid.
410      */
validateRequestCodeForPreferenceController(int requestCode)411     private static void validateRequestCodeForPreferenceController(int requestCode) {
412         if ((requestCode & 0xff00) != 0) {
413             throw new IllegalArgumentException("Can only use lower 8 bits for requestCode");
414         }
415     }
416 
getFragmentHost()417     private FragmentHost getFragmentHost() {
418         return (FragmentHost) requireActivity();
419     }
420 }
421