1 /* 2 * Copyright (C) 2015 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 android.support.v7.preference; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.support.annotation.Nullable; 25 import android.support.annotation.XmlRes; 26 import android.support.v4.app.DialogFragment; 27 import android.support.v4.app.Fragment; 28 import android.support.v7.widget.LinearLayoutManager; 29 import android.support.v7.widget.RecyclerView; 30 import android.util.TypedValue; 31 import android.view.ContextThemeWrapper; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 36 /** 37 * Shows a hierarchy of {@link Preference} objects as 38 * lists. These preferences will 39 * automatically save to {@link android.content.SharedPreferences} as the user interacts with 40 * them. To retrieve an instance of {@link android.content.SharedPreferences} that the 41 * preference hierarchy in this fragment will use, call 42 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} 43 * with a context in the same package as this fragment. 44 * <p> 45 * Furthermore, the preferences shown will follow the visual style of system 46 * preferences. It is easy to create a hierarchy of preferences (that can be 47 * shown on multiple screens) via XML. For these reasons, it is recommended to 48 * use this fragment (as a superclass) to deal with preferences in applications. 49 * <p> 50 * A {@link PreferenceScreen} object should be at the top of the preference 51 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy 52 * denote a screen break--that is the preferences contained within subsequent 53 * {@link PreferenceScreen} should be shown on another screen. The preference 54 * framework handles showing these other screens from the preference hierarchy. 55 * <p> 56 * The preference hierarchy can be formed in multiple ways: 57 * <li> From an XML file specifying the hierarchy 58 * <li> From different {@link android.app.Activity Activities} that each specify its own 59 * preferences in an XML file via {@link android.app.Activity} meta-data 60 * <li> From an object hierarchy rooted with {@link PreferenceScreen} 61 * <p> 62 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The 63 * root element should be a {@link PreferenceScreen}. Subsequent elements can point 64 * to actual {@link Preference} subclasses. As mentioned above, subsequent 65 * {@link PreferenceScreen} in the hierarchy will result in the screen break. 66 * <p> 67 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use 68 * {@link #setPreferenceScreen(PreferenceScreen)}. 69 * <p> 70 * As a convenience, this fragment implements a click listener for any 71 * preference in the current hierarchy, see 72 * {@link #onPreferenceTreeClick(Preference)}. 73 * 74 * <div class="special reference"> 75 * <h3>Developer Guides</h3> 76 * <p>For information about using {@code PreferenceFragment}, 77 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a> 78 * guide.</p> 79 * </div> 80 * 81 * <a name="SampleCode"></a> 82 * <h3>Sample Code</h3> 83 * 84 * <p>The following sample code shows a simple preference fragment that is 85 * populated from a resource. The resource it loads is:</p> 86 * 87 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences} 88 * 89 * <p>The fragment implementation itself simply populates the preferences 90 * when created. Note that the preferences framework takes care of loading 91 * the current values out of the app preferences and writing them when changed:</p> 92 * 93 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java 94 * fragment} 95 * 96 * @see Preference 97 * @see PreferenceScreen 98 */ 99 public abstract class PreferenceFragmentCompat extends Fragment implements 100 PreferenceManager.OnPreferenceTreeClickListener, 101 PreferenceManager.OnDisplayPreferenceDialogListener, 102 PreferenceManager.OnNavigateToScreenListener, 103 DialogPreference.TargetFragment { 104 105 /** 106 * Fragment argument used to specify the tag of the desired root 107 * {@link android.support.v7.preference.PreferenceScreen} object. 108 */ 109 public static final String ARG_PREFERENCE_ROOT = 110 "android.support.v7.preference.PreferenceFragmentCompat.PREFERENCE_ROOT"; 111 112 private static final String PREFERENCES_TAG = "android:preferences"; 113 114 private static final String DIALOG_FRAGMENT_TAG = 115 "android.support.v7.preference.PreferenceFragment.DIALOG"; 116 117 private PreferenceManager mPreferenceManager; 118 private RecyclerView mList; 119 private boolean mHavePrefs; 120 private boolean mInitDone; 121 122 private Context mStyledContext; 123 124 private int mLayoutResId = R.layout.preference_list_fragment; 125 126 /** 127 * The starting request code given out to preference framework. 128 */ 129 private static final int FIRST_REQUEST_CODE = 100; 130 131 private static final int MSG_BIND_PREFERENCES = 1; 132 private Handler mHandler = new Handler() { 133 @Override 134 public void handleMessage(Message msg) { 135 switch (msg.what) { 136 137 case MSG_BIND_PREFERENCES: 138 bindPreferences(); 139 break; 140 } 141 } 142 }; 143 144 final private Runnable mRequestFocus = new Runnable() { 145 public void run() { 146 mList.focusableViewAvailable(mList); 147 } 148 }; 149 150 /** 151 * Interface that PreferenceFragment's containing activity should 152 * implement to be able to process preference items that wish to 153 * switch to a specified fragment. 154 */ 155 public interface OnPreferenceStartFragmentCallback { 156 /** 157 * Called when the user has clicked on a Preference that has 158 * a fragment class name associated with it. The implementation 159 * should instantiate and switch to an instance of the given 160 * fragment. 161 * @param caller The fragment requesting navigation. 162 * @param pref The preference requesting the fragment. 163 * @return true if the fragment creation has been handled 164 */ onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)165 boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref); 166 } 167 168 /** 169 * Interface that PreferenceFragment's containing activity should 170 * implement to be able to process preference items that wish to 171 * switch to a new screen of preferences. 172 */ 173 public interface OnPreferenceStartScreenCallback { 174 /** 175 * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new 176 * screen of preferences. 177 * @param caller The fragment requesting navigation. 178 * @param pref The preference screen to navigate to. 179 * @return true if the screen navigation has been handled 180 */ onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref)181 boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref); 182 } 183 184 public interface OnPreferenceDisplayDialogCallback { 185 186 /** 187 * 188 * @param caller The fragment containing the preference requesting the dialog. 189 * @param pref The preference requesting the dialog. 190 * @return true if the dialog creation has been handled. 191 */ onPreferenceDisplayDialog(PreferenceFragmentCompat caller, Preference pref)192 boolean onPreferenceDisplayDialog(PreferenceFragmentCompat caller, Preference pref); 193 } 194 195 @Override onCreate(Bundle savedInstanceState)196 public void onCreate(Bundle savedInstanceState) { 197 super.onCreate(savedInstanceState); 198 final TypedValue tv = new TypedValue(); 199 getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); 200 final int theme = tv.resourceId; 201 if (theme <= 0) { 202 throw new IllegalStateException("Must specify preferenceTheme in theme"); 203 } 204 mStyledContext = new ContextThemeWrapper(getActivity(), theme); 205 206 mPreferenceManager = new PreferenceManager(mStyledContext); 207 mPreferenceManager.setOnNavigateToScreenListener(this); 208 final Bundle args = getArguments(); 209 final String rootKey; 210 if (args != null) { 211 rootKey = getArguments().getString(ARG_PREFERENCE_ROOT); 212 } else { 213 rootKey = null; 214 } 215 onCreatePreferences(savedInstanceState, rootKey); 216 } 217 218 /** 219 * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment. 220 * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either 221 * directly or via helper methods such as {@link #addPreferencesFromResource(int)}. 222 * 223 * @param savedInstanceState If the fragment is being re-created from 224 * a previous saved state, this is the state. 225 * @param rootKey If non-null, this preference fragment should be rooted at the 226 * {@link android.support.v7.preference.PreferenceScreen} with this key. 227 */ onCreatePreferences(Bundle savedInstanceState, String rootKey)228 public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey); 229 230 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)231 public View onCreateView(LayoutInflater inflater, ViewGroup container, 232 Bundle savedInstanceState) { 233 234 TypedArray a = mStyledContext.obtainStyledAttributes(null, 235 R.styleable.PreferenceFragmentCompat, 236 R.attr.preferenceFragmentStyle, 237 0); 238 239 mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_layout, 240 mLayoutResId); 241 242 a.recycle(); 243 244 final View view = inflater.inflate(mLayoutResId, container, false); 245 246 final View rawListContainer = view.findViewById(R.id.list_container); 247 if (!(rawListContainer instanceof ViewGroup)) { 248 throw new RuntimeException("Content has view with id attribute 'R.id.list_container' " 249 + "that is not a ViewGroup class"); 250 } 251 252 final ViewGroup listContainer = (ViewGroup) rawListContainer; 253 254 final RecyclerView listView = onCreateRecyclerView(inflater, listContainer, 255 savedInstanceState); 256 if (listView == null) { 257 throw new RuntimeException("Could not create RecyclerView"); 258 } 259 260 mList = listView; 261 listContainer.addView(mList); 262 mHandler.post(mRequestFocus); 263 return view; 264 } 265 266 @Override onActivityCreated(Bundle savedInstanceState)267 public void onActivityCreated(Bundle savedInstanceState) { 268 super.onActivityCreated(savedInstanceState); 269 270 if (mHavePrefs) { 271 bindPreferences(); 272 } 273 274 mInitDone = true; 275 276 if (savedInstanceState != null) { 277 Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); 278 if (container != null) { 279 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 280 if (preferenceScreen != null) { 281 preferenceScreen.restoreHierarchyState(container); 282 } 283 } 284 } 285 } 286 287 @Override onStart()288 public void onStart() { 289 super.onStart(); 290 mPreferenceManager.setOnPreferenceTreeClickListener(this); 291 mPreferenceManager.setOnDisplayPreferenceDialogListener(this); 292 } 293 294 @Override onStop()295 public void onStop() { 296 super.onStop(); 297 mPreferenceManager.setOnPreferenceTreeClickListener(null); 298 mPreferenceManager.setOnDisplayPreferenceDialogListener(null); 299 } 300 301 @Override onDestroyView()302 public void onDestroyView() { 303 mList = null; 304 mHandler.removeCallbacks(mRequestFocus); 305 mHandler.removeMessages(MSG_BIND_PREFERENCES); 306 super.onDestroyView(); 307 } 308 309 @Override onSaveInstanceState(Bundle outState)310 public void onSaveInstanceState(Bundle outState) { 311 super.onSaveInstanceState(outState); 312 313 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 314 if (preferenceScreen != null) { 315 Bundle container = new Bundle(); 316 preferenceScreen.saveHierarchyState(container); 317 outState.putBundle(PREFERENCES_TAG, container); 318 } 319 } 320 321 /** 322 * Returns the {@link PreferenceManager} used by this fragment. 323 * @return The {@link PreferenceManager}. 324 */ getPreferenceManager()325 public PreferenceManager getPreferenceManager() { 326 return mPreferenceManager; 327 } 328 329 /** 330 * Sets the root of the preference hierarchy that this fragment is showing. 331 * 332 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 333 */ setPreferenceScreen(PreferenceScreen preferenceScreen)334 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 335 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 336 onUnbindPreferences(); 337 mHavePrefs = true; 338 if (mInitDone) { 339 postBindPreferences(); 340 } 341 } 342 } 343 344 /** 345 * Gets the root of the preference hierarchy that this fragment is showing. 346 * 347 * @return The {@link PreferenceScreen} that is the root of the preference 348 * hierarchy. 349 */ getPreferenceScreen()350 public PreferenceScreen getPreferenceScreen() { 351 return mPreferenceManager.getPreferenceScreen(); 352 } 353 354 /** 355 * Inflates the given XML resource and adds the preference hierarchy to the current 356 * preference hierarchy. 357 * 358 * @param preferencesResId The XML resource ID to inflate. 359 */ addPreferencesFromResource(@mlRes int preferencesResId)360 public void addPreferencesFromResource(@XmlRes int preferencesResId) { 361 requirePreferenceManager(); 362 363 setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext, 364 preferencesResId, getPreferenceScreen())); 365 } 366 367 /** 368 * Inflates the given XML resource and replaces the current preference hierarchy (if any) with 369 * the preference hierarchy rooted at {@code key}. 370 * 371 * @param preferencesResId The XML resource ID to inflate. 372 * @param key The preference key of the {@link android.support.v7.preference.PreferenceScreen} 373 * to use as the root of the preference hierarchy, or null to use the root 374 * {@link android.support.v7.preference.PreferenceScreen}. 375 */ setPreferencesFromResource(@mlRes int preferencesResId, @Nullable String key)376 public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) { 377 requirePreferenceManager(); 378 379 final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext, 380 preferencesResId, null); 381 382 final Preference root; 383 if (key != null) { 384 root = xmlRoot.findPreference(key); 385 if (!(root instanceof PreferenceScreen)) { 386 throw new IllegalArgumentException("Preference object with key " + key 387 + " is not a PreferenceScreen"); 388 } 389 } else { 390 root = xmlRoot; 391 } 392 393 setPreferenceScreen((PreferenceScreen) root); 394 } 395 396 /** 397 * {@inheritDoc} 398 */ onPreferenceTreeClick(Preference preference)399 public boolean onPreferenceTreeClick(Preference preference) { 400 if (preference.getFragment() != null) { 401 boolean handled = false; 402 if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) { 403 handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment()) 404 .onPreferenceStartFragment(this, preference); 405 } 406 if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback){ 407 handled = ((OnPreferenceStartFragmentCallback) getActivity()) 408 .onPreferenceStartFragment(this, preference); 409 } 410 return handled; 411 } 412 return false; 413 } 414 415 /** 416 * Called by 417 * {@link android.support.v7.preference.PreferenceScreen#onClick()} in order to navigate to a 418 * new screen of preferences. Calls 419 * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen} 420 * if the target fragment or containing activity implements 421 * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}. 422 * @param preferenceScreen The {@link android.support.v7.preference.PreferenceScreen} to 423 * navigate to. 424 */ 425 @Override onNavigateToScreen(PreferenceScreen preferenceScreen)426 public void onNavigateToScreen(PreferenceScreen preferenceScreen) { 427 boolean handled = false; 428 if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) { 429 handled = ((OnPreferenceStartScreenCallback) getCallbackFragment()) 430 .onPreferenceStartScreen(this, preferenceScreen); 431 } 432 if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) { 433 ((OnPreferenceStartScreenCallback) getActivity()) 434 .onPreferenceStartScreen(this, preferenceScreen); 435 } 436 } 437 438 /** 439 * Finds a {@link Preference} based on its key. 440 * 441 * @param key The key of the preference to retrieve. 442 * @return The {@link Preference} with the key, or null. 443 * @see android.support.v7.preference.PreferenceGroup#findPreference(CharSequence) 444 */ findPreference(CharSequence key)445 public Preference findPreference(CharSequence key) { 446 if (mPreferenceManager == null) { 447 return null; 448 } 449 return mPreferenceManager.findPreference(key); 450 } 451 requirePreferenceManager()452 private void requirePreferenceManager() { 453 if (mPreferenceManager == null) { 454 throw new RuntimeException("This should be called after super.onCreate."); 455 } 456 } 457 postBindPreferences()458 private void postBindPreferences() { 459 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 460 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 461 } 462 bindPreferences()463 private void bindPreferences() { 464 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 465 if (preferenceScreen != null) { 466 getListView().setAdapter(onCreateAdapter(preferenceScreen)); 467 preferenceScreen.onAttached(); 468 } 469 onBindPreferences(); 470 } 471 472 /** @hide */ onBindPreferences()473 protected void onBindPreferences() { 474 } 475 476 /** @hide */ onUnbindPreferences()477 protected void onUnbindPreferences() { 478 } 479 getListView()480 public final RecyclerView getListView() { 481 return mList; 482 } 483 484 /** 485 * Creates the {@link android.support.v7.widget.RecyclerView} used to display the preferences. 486 * Subclasses may override this to return a customized 487 * {@link android.support.v7.widget.RecyclerView}. 488 * @param inflater The LayoutInflater object that can be used to inflate the 489 * {@link android.support.v7.widget.RecyclerView}. 490 * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to. 491 * This method should not add the view itself, but this can be used to generate 492 * the LayoutParams of the view. 493 * @param savedInstanceState If non-null, this view is being re-constructed from a previous 494 * saved state as given here 495 * @return A new RecyclerView object to be placed into the view hierarchy 496 */ onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)497 public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, 498 Bundle savedInstanceState) { 499 RecyclerView recyclerView = (RecyclerView) inflater 500 .inflate(R.layout.preference_recyclerview, parent, false); 501 502 recyclerView.setLayoutManager(onCreateLayoutManager()); 503 504 return recyclerView; 505 } 506 507 /** 508 * Called from {@link #onCreateRecyclerView} to create the 509 * {@link android.support.v7.widget.RecyclerView.LayoutManager} for the created 510 * {@link android.support.v7.widget.RecyclerView}. 511 * @return A new {@link android.support.v7.widget.RecyclerView.LayoutManager} instance. 512 */ onCreateLayoutManager()513 public RecyclerView.LayoutManager onCreateLayoutManager() { 514 return new LinearLayoutManager(getActivity()); 515 } 516 517 /** 518 * Creates the root adapter. 519 * 520 * @param preferenceScreen Preference screen object to create the adapter for. 521 * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}. 522 */ onCreateAdapter(PreferenceScreen preferenceScreen)523 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { 524 return new PreferenceGroupAdapter(preferenceScreen); 525 } 526 527 /** 528 * Called when a preference in the tree requests to display a dialog. Subclasses should 529 * override this method to display custom dialogs or to handle dialogs for custom preference 530 * classes. 531 * 532 * @param preference The Preference object requesting the dialog. 533 */ 534 @Override onDisplayPreferenceDialog(Preference preference)535 public void onDisplayPreferenceDialog(Preference preference) { 536 537 boolean handled = false; 538 if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) { 539 handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment()) 540 .onPreferenceDisplayDialog(this, preference); 541 } 542 if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) { 543 handled = ((OnPreferenceDisplayDialogCallback) getActivity()) 544 .onPreferenceDisplayDialog(this, preference); 545 } 546 547 if (handled) { 548 return; 549 } 550 551 // check if dialog is already showing 552 if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) { 553 return; 554 } 555 556 final DialogFragment f; 557 if (preference instanceof EditTextPreference) { 558 f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey()); 559 } else if (preference instanceof ListPreference) { 560 f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey()); 561 } else { 562 throw new IllegalArgumentException("Tried to display dialog for unknown " + 563 "preference type. Did you forget to override onDisplayPreferenceDialog()?"); 564 } 565 f.setTargetFragment(this, 0); 566 f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); 567 } 568 569 /** 570 * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib. 571 * @return Fragment to possibly use as a callback 572 * @hide 573 */ getCallbackFragment()574 public Fragment getCallbackFragment() { 575 return null; 576 } 577 } 578