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