• 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;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentSender;
24 import android.os.Bundle;
25 import android.util.ArrayMap;
26 import android.util.SparseArray;
27 import android.util.TypedValue;
28 import android.view.ContextThemeWrapper;
29 import android.widget.FrameLayout;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 import android.widget.Toast;
33 
34 import androidx.annotation.LayoutRes;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.StringRes;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.annotation.XmlRes;
39 import androidx.constraintlayout.widget.Guideline;
40 import androidx.fragment.app.DialogFragment;
41 import androidx.fragment.app.Fragment;
42 import androidx.fragment.app.FragmentManager;
43 import androidx.lifecycle.Lifecycle;
44 import androidx.preference.EditTextPreference;
45 import androidx.preference.ListPreference;
46 import androidx.preference.Preference;
47 import androidx.preference.PreferenceFragmentCompat;
48 import androidx.preference.PreferenceScreen;
49 
50 import com.android.car.settings.R;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Map;
55 
56 /**
57  * Base fragment for all settings. Subclasses must provide a resource id via
58  * {@link #getPreferenceScreenResId()} for the XML resource which defines the preferences to
59  * display and controllers to update their state. This class is responsible for displaying the
60  * preferences, creating {@link PreferenceController} instances from the metadata, and
61  * associating the preferences with their corresponding controllers.
62  *
63  * <p>{@code preferenceTheme} must be specified in the application theme, and the parent to which
64  * this fragment attaches must implement {@link UxRestrictionsProvider} and
65  * {@link FragmentController} or an {@link IllegalStateException} will be thrown during
66  * {@link #onAttach(Context)}. Changes to driving state restrictions are propagated to
67  * controllers.
68  */
69 public abstract class SettingsFragment extends PreferenceFragmentCompat implements
70         CarUxRestrictionsManager.OnUxRestrictionsChangedListener, FragmentController {
71 
72     @VisibleForTesting
73     static final String DIALOG_FRAGMENT_TAG =
74             "com.android.car.settings.common.SettingsFragment.DIALOG";
75 
76     private static final int MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS = 0xff - 1;
77 
78     private final Map<Class, List<PreferenceController>> mPreferenceControllersLookup =
79             new ArrayMap<>();
80     private final List<PreferenceController> mPreferenceControllers = new ArrayList<>();
81     private final SparseArray<ActivityResultCallback> mActivityResultCallbackMap =
82             new SparseArray<>();
83 
84     private CarUxRestrictions mUxRestrictions;
85     private int mCurrentRequestIndex = 0;
86 
87     /**
88      * Returns the resource id for the preference XML of this fragment.
89      */
90     @XmlRes
getPreferenceScreenResId()91     protected abstract int getPreferenceScreenResId();
92 
93     /**
94      * Returns the layout id to use as the activity action bar. Subclasses should override this
95      * method to customize the action bar layout (e.g. additional buttons, switches, etc.). The
96      * default action bar contains a back button and the title.
97      */
98     @LayoutRes
getActionBarLayoutId()99     protected int getActionBarLayoutId() {
100         return R.layout.action_bar;
101     }
102 
103     /**
104      * Returns the controller of the given {@code clazz} for the given {@code
105      * preferenceKeyResId}. Subclasses may use this method in {@link #onAttach(Context)} to call
106      * setters on controllers to pass additional arguments after construction.
107      *
108      * <p>For example:
109      * <pre>{@code
110      * @Override
111      * public void onAttach(Context context) {
112      *     super.onAttach(context);
113      *     use(MyPreferenceController.class, R.string.pk_my_key).setMyArg(myArg);
114      * }
115      * }</pre>
116      *
117      * <p>Important: Use judiciously to minimize tight coupling between controllers and fragments.
118      */
119     @SuppressWarnings("unchecked") // Class is used as map key.
use(Class<T> clazz, @StringRes int preferenceKeyResId)120     protected <T extends PreferenceController> T use(Class<T> clazz,
121             @StringRes int preferenceKeyResId) {
122         List<PreferenceController> controllerList = mPreferenceControllersLookup.get(clazz);
123         if (controllerList != null) {
124             String preferenceKey = getString(preferenceKeyResId);
125             for (PreferenceController controller : controllerList) {
126                 if (controller.getPreferenceKey().equals(preferenceKey)) {
127                     return (T) controller;
128                 }
129             }
130         }
131         return null;
132     }
133 
134     @Override
onAttach(Context context)135     public void onAttach(Context context) {
136         super.onAttach(context);
137         if (!(getActivity() instanceof UxRestrictionsProvider)) {
138             throw new IllegalStateException("Must attach to a UxRestrictionsProvider");
139         }
140         if (!(getActivity() instanceof FragmentController)) {
141             throw new IllegalStateException("Must attach to a FragmentController");
142         }
143 
144         TypedValue tv = new TypedValue();
145         getActivity().getTheme().resolveAttribute(androidx.preference.R.attr.preferenceTheme, tv,
146                 true);
147         int theme = tv.resourceId;
148         if (theme == 0) {
149             throw new IllegalStateException("Must specify preferenceTheme in theme");
150         }
151         // Construct a context with the theme as controllers may create new preferences.
152         Context styledContext = new ContextThemeWrapper(getActivity(), theme);
153 
154         mUxRestrictions = ((UxRestrictionsProvider) requireActivity()).getCarUxRestrictions();
155         mPreferenceControllers.clear();
156         mPreferenceControllers.addAll(
157                 PreferenceControllerListHelper.getPreferenceControllersFromXml(styledContext,
158                         getPreferenceScreenResId(), /* fragmentController= */ this,
159                         mUxRestrictions));
160 
161         Lifecycle lifecycle = getLifecycle();
162         mPreferenceControllers.forEach(controller -> {
163             lifecycle.addObserver(controller);
164             mPreferenceControllersLookup.computeIfAbsent(controller.getClass(),
165                     k -> new ArrayList<>(/* initialCapacity= */ 1)).add(controller);
166         });
167     }
168 
169     /**
170      * Inflates the preferences from {@link #getPreferenceScreenResId()} and associates the
171      * preference with their corresponding {@link PreferenceController} instances.
172      */
173     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)174     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
175         @XmlRes int resId = getPreferenceScreenResId();
176         if (resId <= 0) {
177             throw new IllegalStateException(
178                     "Fragment must specify a preference screen resource ID");
179         }
180         addPreferencesFromResource(resId);
181         PreferenceScreen screen = getPreferenceScreen();
182         for (PreferenceController controller : mPreferenceControllers) {
183             controller.setPreference(screen.findPreference(controller.getPreferenceKey()));
184         }
185     }
186 
187     @Override
onActivityCreated(Bundle savedInstanceState)188     public void onActivityCreated(Bundle savedInstanceState) {
189         super.onActivityCreated(savedInstanceState);
190         FrameLayout actionBarContainer = requireActivity().findViewById(R.id.action_bar);
191         if (actionBarContainer != null) {
192             actionBarContainer.removeAllViews();
193             getLayoutInflater().inflate(getActionBarLayoutId(), actionBarContainer);
194 
195             TextView titleView = actionBarContainer.requireViewById(R.id.title);
196             titleView.setText(getPreferenceScreen().getTitle());
197 
198             // If the fragment is root, change the back button to settings icon.
199             ImageView imageView = actionBarContainer.requireViewById(R.id.back_button);
200             FragmentManager fragmentManager = requireActivity().getSupportFragmentManager();
201             if (fragmentManager.getBackStackEntryCount() == 1
202                     && fragmentManager.findFragmentByTag("0") != null
203                     && fragmentManager.findFragmentByTag("0").getClass().getName().equals(
204                     getString(R.string.config_settings_hierarchy_root_fragment))) {
205                 if (getContext().getResources()
206                         .getBoolean(R.bool.config_show_settings_root_exit_icon)) {
207                     imageView.setImageResource(R.drawable.ic_launcher_settings);
208                     imageView.setTag(R.id.back_button, R.drawable.ic_launcher_settings);
209                 } else {
210                     hideExitIcon();
211                 }
212             } else {
213                 imageView.setTag(R.id.back_button, R.drawable.ic_arrow_back);
214                 actionBarContainer.requireViewById(R.id.action_bar_icon_container)
215                         .setOnClickListener(
216                                 v -> requireActivity().onBackPressed());
217             }
218         }
219     }
220 
221     @Override
onDetach()222     public void onDetach() {
223         super.onDetach();
224         Lifecycle lifecycle = getLifecycle();
225         mPreferenceControllers.forEach(lifecycle::removeObserver);
226         mActivityResultCallbackMap.clear();
227     }
228 
229     /**
230      * Notifies {@link PreferenceController} instances of changes to {@link CarUxRestrictions}.
231      */
232     @Override
onUxRestrictionsChanged(CarUxRestrictions uxRestrictions)233     public void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) {
234         if (!uxRestrictions.isSameRestrictions(mUxRestrictions)) {
235             mUxRestrictions = uxRestrictions;
236             for (PreferenceController controller : mPreferenceControllers) {
237                 controller.onUxRestrictionsChanged(uxRestrictions);
238             }
239         }
240     }
241 
242     /**
243      * {@inheritDoc}
244      *
245      * <p>Settings needs to launch custom dialog types in order to extend the Device Default theme.
246      *
247      * @param preference The Preference object requesting the dialog.
248      */
249     @Override
onDisplayPreferenceDialog(Preference preference)250     public void onDisplayPreferenceDialog(Preference preference) {
251         // check if dialog is already showing
252         if (findDialogByTag(DIALOG_FRAGMENT_TAG) != null) {
253             return;
254         }
255 
256         DialogFragment dialogFragment;
257         if (preference instanceof ValidatedEditTextPreference) {
258             if (preference instanceof PasswordEditTextPreference) {
259                 dialogFragment = PasswordEditTextPreferenceDialogFragment.newInstance(
260                         preference.getKey());
261             } else {
262                 dialogFragment = ValidatedEditTextPreferenceDialogFragment.newInstance(
263                         preference.getKey());
264             }
265         } else if (preference instanceof EditTextPreference) {
266             dialogFragment = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
267         } else if (preference instanceof ListPreference) {
268             dialogFragment = SettingsListPreferenceDialogFragment.newInstance(preference.getKey());
269         } else {
270             throw new IllegalArgumentException(
271                     "Tried to display dialog for unknown preference type. Did you forget to "
272                             + "override onDisplayPreferenceDialog()?");
273         }
274 
275         dialogFragment.setTargetFragment(/* fragment= */ this, /* requestCode= */ 0);
276         showDialog(dialogFragment, DIALOG_FRAGMENT_TAG);
277     }
278 
279     @Override
launchFragment(Fragment fragment)280     public void launchFragment(Fragment fragment) {
281         ((FragmentController) requireActivity()).launchFragment(fragment);
282     }
283 
284     @Override
goBack()285     public void goBack() {
286         requireActivity().onBackPressed();
287     }
288 
289     @Override
showBlockingMessage()290     public void showBlockingMessage() {
291         Toast.makeText(getContext(), R.string.restricted_while_driving, Toast.LENGTH_SHORT).show();
292     }
293 
294     @Override
showDialog(DialogFragment dialogFragment, @Nullable String tag)295     public void showDialog(DialogFragment dialogFragment, @Nullable String tag) {
296         dialogFragment.show(getFragmentManager(), tag);
297     }
298 
299     @Nullable
300     @Override
findDialogByTag(String tag)301     public DialogFragment findDialogByTag(String tag) {
302         Fragment fragment = getFragmentManager().findFragmentByTag(tag);
303         if (fragment instanceof DialogFragment) {
304             return (DialogFragment) fragment;
305         }
306         return null;
307     }
308 
309     @Override
startActivityForResult(Intent intent, int requestCode, ActivityResultCallback callback)310     public void startActivityForResult(Intent intent, int requestCode,
311             ActivityResultCallback callback) {
312         validateRequestCodeForPreferenceController(requestCode);
313         int requestIndex = allocateRequestIndex(callback);
314         super.startActivityForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff));
315     }
316 
317     @Override
startIntentSenderForResult(IntentSender intent, int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, Bundle options, ActivityResultCallback callback)318     public void startIntentSenderForResult(IntentSender intent, int requestCode,
319             @Nullable Intent fillInIntent, int flagsMask, int flagsValues, Bundle options,
320             ActivityResultCallback callback)
321             throws IntentSender.SendIntentException {
322         validateRequestCodeForPreferenceController(requestCode);
323         int requestIndex = allocateRequestIndex(callback);
324         super.startIntentSenderForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff),
325                 fillInIntent, flagsMask, flagsValues, /* extraFlags= */ 0, options);
326     }
327 
328     @Override
onActivityResult(int requestCode, int resultCode, Intent data)329     public void onActivityResult(int requestCode, int resultCode, Intent data) {
330         super.onActivityResult(requestCode, resultCode, data);
331         int requestIndex = (requestCode >> 8) & 0xff;
332         if (requestIndex != 0) {
333             requestIndex--;
334             ActivityResultCallback callback = mActivityResultCallbackMap.get(requestIndex);
335             mActivityResultCallbackMap.remove(requestIndex);
336             if (callback != null) {
337                 callback.processActivityResult(requestCode & 0xff, resultCode, data);
338             }
339         }
340     }
341 
342     // Allocates the next available startActivityForResult request index.
allocateRequestIndex(ActivityResultCallback callback)343     private int allocateRequestIndex(ActivityResultCallback callback) {
344         // Sanity check that we haven't exhausted the request index space.
345         if (mActivityResultCallbackMap.size() >= MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS) {
346             throw new IllegalStateException(
347                     "Too many pending activity result callbacks.");
348         }
349 
350         // Find an unallocated request index in the mPendingFragmentActivityResults map.
351         while (mActivityResultCallbackMap.indexOfKey(mCurrentRequestIndex) >= 0) {
352             mCurrentRequestIndex =
353                     (mCurrentRequestIndex + 1) % MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS;
354         }
355 
356         mActivityResultCallbackMap.put(mCurrentRequestIndex, callback);
357         return mCurrentRequestIndex;
358     }
359 
360     /**
361      * Checks whether the given request code is a valid code by masking it with 0xff00. Throws an
362      * {@link IllegalArgumentException} if the code is not valid.
363      */
validateRequestCodeForPreferenceController(int requestCode)364     private static void validateRequestCodeForPreferenceController(int requestCode) {
365         if ((requestCode & 0xff00) != 0) {
366             throw new IllegalArgumentException("Can only use lower 8 bits for requestCode");
367         }
368     }
369 
hideExitIcon()370     private void hideExitIcon() {
371         requireActivity().findViewById(R.id.action_bar_icon_container)
372                 .setVisibility(FrameLayout.GONE);
373 
374         Guideline guideLine = (Guideline) requireActivity().findViewById(R.id.start_margin);
375         guideLine.setGuidelineBegin(getResources()
376                 .getDimensionPixelOffset(R.dimen.action_bar_no_icon_start_margin));
377     }
378 }
379