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