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