| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.settings.common; |
| |
| import static com.android.car.settings.common.BaseCarSettingsActivity.META_DATA_KEY_SINGLE_PANE; |
| |
| import android.car.drivingstate.CarUxRestrictions; |
| import android.car.drivingstate.CarUxRestrictionsManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.os.Bundle; |
| import android.util.ArrayMap; |
| import android.util.SparseArray; |
| import android.util.TypedValue; |
| import android.view.ContextThemeWrapper; |
| import android.view.LayoutInflater; |
| import android.view.ViewGroup; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.StringRes; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.annotation.XmlRes; |
| import androidx.fragment.app.DialogFragment; |
| import androidx.fragment.app.Fragment; |
| import androidx.lifecycle.Lifecycle; |
| import androidx.preference.Preference; |
| import androidx.preference.PreferenceScreen; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.car.settings.R; |
| import com.android.car.ui.baselayout.Insets; |
| import com.android.car.ui.preference.PreferenceFragment; |
| import com.android.car.ui.recyclerview.CarUiRecyclerView; |
| import com.android.car.ui.toolbar.MenuItem; |
| import com.android.car.ui.toolbar.NavButtonMode; |
| import com.android.car.ui.toolbar.ToolbarController; |
| import com.android.car.ui.utils.ViewUtils; |
| import com.android.settingslib.search.Indexable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Base fragment for all settings. Subclasses must provide a resource id via |
| * {@link #getPreferenceScreenResId()} for the XML resource which defines the preferences to |
| * display and controllers to update their state. This class is responsible for displaying the |
| * preferences, creating {@link PreferenceController} instances from the metadata, and |
| * associating the preferences with their corresponding controllers. |
| * |
| * <p>{@code preferenceTheme} must be specified in the application theme, and the parent to which |
| * this fragment attaches must implement {@link UxRestrictionsProvider} and |
| * {@link FragmentController} or an {@link IllegalStateException} will be thrown during |
| * {@link #onAttach(Context)}. Changes to driving state restrictions are propagated to |
| * controllers. |
| */ |
| public abstract class SettingsFragment extends PreferenceFragment implements |
| CarUxRestrictionsManager.OnUxRestrictionsChangedListener, FragmentController, Indexable { |
| |
| @VisibleForTesting |
| static final String DIALOG_FRAGMENT_TAG = |
| "com.android.car.settings.common.SettingsFragment.DIALOG"; |
| |
| private static final int MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS = 0xff - 1; |
| |
| private final Map<String, PreferenceController> mPreferenceControllersLookup = new ArrayMap<>(); |
| private final List<PreferenceController> mPreferenceControllers = new ArrayList<>(); |
| private final SparseArray<ActivityResultCallback> mActivityResultCallbackMap = |
| new SparseArray<>(); |
| |
| private CarUxRestrictions mUxRestrictions; |
| private HighlightablePreferenceGroupAdapter mAdapter; |
| private int mCurrentRequestIndex = 0; |
| |
| /** |
| * Returns the resource id for the preference XML of this fragment. |
| */ |
| @XmlRes |
| protected abstract int getPreferenceScreenResId(); |
| |
| protected ToolbarController getToolbar() { |
| return getFragmentHost().getToolbar(); |
| } |
| /** |
| * Returns the MenuItems to display in the toolbar. Subclasses should override this to |
| * add additional buttons, switches, ect. to the toolbar. |
| */ |
| protected List<MenuItem> getToolbarMenuItems() { |
| return null; |
| } |
| |
| /** |
| * Returns the controller of the given {@code clazz} for the given {@code |
| * preferenceKeyResId}. Subclasses may use this method in {@link #onAttach(Context)} to call |
| * setters on controllers to pass additional arguments after construction. |
| * |
| * <p>For example: |
| * <pre>{@code |
| * @Override |
| * public void onAttach(Context context) { |
| * super.onAttach(context); |
| * use(MyPreferenceController.class, R.string.pk_my_key).setMyArg(myArg); |
| * } |
| * }</pre> |
| * |
| * <p>Important: Use judiciously to minimize tight coupling between controllers and fragments. |
| */ |
| @SuppressWarnings("unchecked") // PreferenceKey is the map key |
| protected <T extends PreferenceController> T use(Class<T> clazz, |
| @StringRes int preferenceKeyResId) { |
| String preferenceKey = getString(preferenceKeyResId); |
| return (T) mPreferenceControllersLookup.get(preferenceKey); |
| } |
| |
| /** |
| * Enables rotary scrolling for the {@link CarUiRecyclerView} in this fragment. |
| * <p> |
| * Rotary scrolling should be enabled for scrolling views which contain content which the user |
| * may want to see but can't interact with, either alone or along with interactive (focusable) |
| * content. |
| */ |
| protected void enableRotaryScroll() { |
| CarUiRecyclerView recyclerView = getView().findViewById(R.id.settings_recycler_view); |
| if (recyclerView != null) { |
| ViewUtils.setRotaryScrollEnabled(recyclerView.getView(), /* isVertical= */ true); |
| } |
| } |
| |
| @Override |
| public void onAttach(Context context) { |
| super.onAttach(context); |
| if (!(getActivity() instanceof UxRestrictionsProvider)) { |
| throw new IllegalStateException("Must attach to a UxRestrictionsProvider"); |
| } |
| if (!(getActivity() instanceof FragmentHost)) { |
| throw new IllegalStateException("Must attach to a FragmentHost"); |
| } |
| |
| TypedValue tv = new TypedValue(); |
| getActivity().getTheme().resolveAttribute(androidx.preference.R.attr.preferenceTheme, tv, |
| true); |
| int theme = tv.resourceId; |
| if (theme == 0) { |
| throw new IllegalStateException("Must specify preferenceTheme in theme"); |
| } |
| // Construct a context with the theme as controllers may create new preferences. |
| Context styledContext = new ContextThemeWrapper(getActivity(), theme); |
| |
| mUxRestrictions = ((UxRestrictionsProvider) requireActivity()).getCarUxRestrictions(); |
| mPreferenceControllers.clear(); |
| mPreferenceControllers.addAll( |
| PreferenceControllerListHelper.getPreferenceControllersFromXml(styledContext, |
| getPreferenceScreenResId(), /* fragmentController= */ this, |
| mUxRestrictions)); |
| |
| Lifecycle lifecycle = getLifecycle(); |
| mPreferenceControllers.forEach(controller -> { |
| lifecycle.addObserver(controller); |
| mPreferenceControllersLookup.put(controller.getPreferenceKey(), controller); |
| }); |
| } |
| |
| /** |
| * Inflates the preferences from {@link #getPreferenceScreenResId()} and associates the |
| * preference with their corresponding {@link PreferenceController} instances. |
| */ |
| @Override |
| public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |
| @XmlRes int resId = getPreferenceScreenResId(); |
| if (resId <= 0) { |
| throw new IllegalStateException( |
| "Fragment must specify a preference screen resource ID"); |
| } |
| addPreferencesFromResource(resId); |
| PreferenceScreen screen = getPreferenceScreen(); |
| for (PreferenceController controller : mPreferenceControllers) { |
| Preference pref = screen.findPreference(controller.getPreferenceKey()); |
| |
| controller.setPreference(pref); |
| } |
| } |
| |
| @Override |
| public CarUiRecyclerView onCreateCarUiRecyclerView(LayoutInflater inflater, ViewGroup parent, |
| Bundle savedInstanceState) { |
| inflater.inflate(R.layout.settings_recyclerview_default, parent, /* attachToRoot= */ true); |
| return parent.findViewById(R.id.settings_recycler_view); |
| } |
| |
| @Override |
| protected void setupToolbar(@NonNull ToolbarController toolbar) { |
| List<MenuItem> items = getToolbarMenuItems(); |
| if (items != null) { |
| if (items.size() == 1) { |
| items.get(0).setId(R.id.toolbar_menu_item_0); |
| } else if (items.size() == 2) { |
| items.get(0).setId(R.id.toolbar_menu_item_0); |
| items.get(1).setId(R.id.toolbar_menu_item_1); |
| } |
| } |
| toolbar.setTitle(getPreferenceScreen().getTitle()); |
| toolbar.setMenuItems(items); |
| toolbar.setLogo(null); |
| if (getActivity().getIntent().getBooleanExtra(META_DATA_KEY_SINGLE_PANE, false)) { |
| toolbar.setNavButtonMode(NavButtonMode.BACK); |
| } |
| } |
| |
| @Override |
| public void onDetach() { |
| super.onDetach(); |
| Lifecycle lifecycle = getLifecycle(); |
| mPreferenceControllers.forEach(lifecycle::removeObserver); |
| mActivityResultCallbackMap.clear(); |
| } |
| |
| @Override |
| protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { |
| mAdapter = createHighlightableAdapter(preferenceScreen); |
| return mAdapter; |
| } |
| |
| /** |
| * Returns a HighlightablePreferenceGroupAdapter to be used as the RecyclerView.Adapter for |
| * this fragment. Subclasses can override this method to return their own |
| * HighlightablePreferenceGroupAdapter instance. |
| */ |
| protected HighlightablePreferenceGroupAdapter createHighlightableAdapter( |
| PreferenceScreen preferenceScreen) { |
| return new HighlightablePreferenceGroupAdapter(preferenceScreen); |
| } |
| |
| protected void requestPreferenceHighlight(String key) { |
| if (mAdapter != null) { |
| mAdapter.requestHighlight(getView(), getListView(), key); |
| } |
| } |
| |
| protected void clearPreferenceHighlight() { |
| if (mAdapter != null) { |
| mAdapter.clearHighlight(getView()); |
| } |
| } |
| |
| /** |
| * Notifies {@link PreferenceController} instances of changes to {@link CarUxRestrictions}. |
| */ |
| @Override |
| public void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) { |
| if (!uxRestrictions.isSameRestrictions(mUxRestrictions)) { |
| mUxRestrictions = uxRestrictions; |
| for (PreferenceController controller : mPreferenceControllers) { |
| controller.onUxRestrictionsChanged(uxRestrictions); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Settings needs to launch custom dialog types in order to extend the Device Default theme. |
| * |
| * @param preference The Preference object requesting the dialog. |
| */ |
| @Override |
| public void onDisplayPreferenceDialog(Preference preference) { |
| // check if dialog is already showing |
| if (findDialogByTag(DIALOG_FRAGMENT_TAG) != null) { |
| return; |
| } |
| |
| if (preference instanceof ValidatedEditTextPreference) { |
| DialogFragment dialogFragment = preference instanceof PasswordEditTextPreference |
| ? PasswordEditTextPreferenceDialogFragment.newInstance(preference.getKey()) |
| : ValidatedEditTextPreferenceDialogFragment.newInstance(preference.getKey()); |
| |
| dialogFragment.setTargetFragment(/* fragment= */ this, /* requestCode= */ 0); |
| showDialog(dialogFragment, DIALOG_FRAGMENT_TAG); |
| } else { |
| super.onDisplayPreferenceDialog(preference); |
| } |
| } |
| |
| @Override |
| public void launchFragment(Fragment fragment) { |
| getFragmentHost().launchFragment(fragment); |
| } |
| |
| @Override |
| public void goBack() { |
| getFragmentHost().goBack(); |
| } |
| |
| @Override |
| public void showDialog(DialogFragment dialogFragment, @Nullable String tag) { |
| dialogFragment.show(getFragmentManager(), tag); |
| } |
| |
| @Override |
| public void showProgressBar(boolean visible) { |
| if (getToolbar() != null && getToolbar().getProgressBar() != null) { |
| getToolbar().getProgressBar().setVisible(visible); |
| } |
| } |
| |
| @Nullable |
| @Override |
| public DialogFragment findDialogByTag(String tag) { |
| Fragment fragment = getFragmentManager().findFragmentByTag(tag); |
| if (fragment instanceof DialogFragment) { |
| return (DialogFragment) fragment; |
| } |
| return null; |
| } |
| |
| @NonNull |
| @Override |
| public Lifecycle getSettingsLifecycle() { |
| return getLifecycle(); |
| } |
| |
| @Override |
| public void startActivityForResult(Intent intent, int requestCode, |
| ActivityResultCallback callback) { |
| validateRequestCodeForPreferenceController(requestCode); |
| int requestIndex = allocateRequestIndex(callback); |
| super.startActivityForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff)); |
| } |
| |
| @Override |
| public void startIntentSenderForResult(IntentSender intent, int requestCode, |
| @Nullable Intent fillInIntent, int flagsMask, int flagsValues, Bundle options, |
| ActivityResultCallback callback) |
| throws IntentSender.SendIntentException { |
| validateRequestCodeForPreferenceController(requestCode); |
| int requestIndex = allocateRequestIndex(callback); |
| super.startIntentSenderForResult(intent, ((requestIndex + 1) << 8) + (requestCode & 0xff), |
| fillInIntent, flagsMask, flagsValues, /* extraFlags= */ 0, options); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| super.onActivityResult(requestCode, resultCode, data); |
| int requestIndex = (requestCode >> 8) & 0xff; |
| if (requestIndex != 0) { |
| requestIndex--; |
| ActivityResultCallback callback = mActivityResultCallbackMap.get(requestIndex); |
| mActivityResultCallbackMap.remove(requestIndex); |
| if (callback != null) { |
| callback.processActivityResult(requestCode & 0xff, resultCode, data); |
| } |
| } |
| } |
| |
| @Override |
| protected ToolbarController getPreferenceToolbar(@NonNull Fragment fragment) { |
| return getToolbar(); |
| } |
| |
| @Override |
| protected Insets getPreferenceInsets(@NonNull Fragment fragment) { |
| return null; |
| } |
| |
| // Allocates the next available startActivityForResult request index. |
| private int allocateRequestIndex(ActivityResultCallback callback) { |
| // Check that we haven't exhausted the request index space. |
| if (mActivityResultCallbackMap.size() >= MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS) { |
| throw new IllegalStateException( |
| "Too many pending activity result callbacks."); |
| } |
| |
| // Find an unallocated request index in the mPendingFragmentActivityResults map. |
| while (mActivityResultCallbackMap.indexOfKey(mCurrentRequestIndex) >= 0) { |
| mCurrentRequestIndex = |
| (mCurrentRequestIndex + 1) % MAX_NUM_PENDING_ACTIVITY_RESULT_CALLBACKS; |
| } |
| |
| mActivityResultCallbackMap.put(mCurrentRequestIndex, callback); |
| return mCurrentRequestIndex; |
| } |
| |
| /** |
| * Checks whether the given request code is a valid code by masking it with 0xff00. Throws an |
| * {@link IllegalArgumentException} if the code is not valid. |
| */ |
| private static void validateRequestCodeForPreferenceController(int requestCode) { |
| if ((requestCode & 0xff00) != 0) { |
| throw new IllegalArgumentException("Can only use lower 8 bits for requestCode"); |
| } |
| } |
| |
| private FragmentHost getFragmentHost() { |
| return (FragmentHost) requireActivity(); |
| } |
| } |