1 /* 2 * Copyright (C) 2010 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.preference; 18 19 import android.annotation.Nullable; 20 import android.annotation.XmlRes; 21 import android.app.Activity; 22 import android.app.Fragment; 23 import android.content.Intent; 24 import android.content.SharedPreferences; 25 import android.content.res.TypedArray; 26 import android.graphics.drawable.Drawable; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.text.TextUtils; 31 import android.view.KeyEvent; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.View.OnKeyListener; 35 import android.view.ViewGroup; 36 import android.widget.ListView; 37 import android.widget.TextView; 38 39 /** 40 * Shows a hierarchy of {@link Preference} objects as 41 * lists. These preferences will 42 * automatically save to {@link SharedPreferences} as the user interacts with 43 * them. To retrieve an instance of {@link SharedPreferences} that the 44 * preference hierarchy in this fragment will use, call 45 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} 46 * with a context in the same package as this fragment. 47 * <p> 48 * Furthermore, the preferences shown will follow the visual style of system 49 * preferences. It is easy to create a hierarchy of preferences (that can be 50 * shown on multiple screens) via XML. For these reasons, it is recommended to 51 * use this fragment (as a superclass) to deal with preferences in applications. 52 * <p> 53 * A {@link PreferenceScreen} object should be at the top of the preference 54 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy 55 * denote a screen break--that is the preferences contained within subsequent 56 * {@link PreferenceScreen} should be shown on another screen. The preference 57 * framework handles showing these other screens from the preference hierarchy. 58 * <p> 59 * The preference hierarchy can be formed in multiple ways: 60 * <li> From an XML file specifying the hierarchy 61 * <li> From different {@link Activity Activities} that each specify its own 62 * preferences in an XML file via {@link Activity} meta-data 63 * <li> From an object hierarchy rooted with {@link PreferenceScreen} 64 * <p> 65 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The 66 * root element should be a {@link PreferenceScreen}. Subsequent elements can point 67 * to actual {@link Preference} subclasses. As mentioned above, subsequent 68 * {@link PreferenceScreen} in the hierarchy will result in the screen break. 69 * <p> 70 * To specify an {@link Intent} to query {@link Activity Activities} that each 71 * have preferences, use {@link #addPreferencesFromIntent}. Each 72 * {@link Activity} can specify meta-data in the manifest (via the key 73 * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML 74 * resource. These XML resources will be inflated into a single preference 75 * hierarchy and shown by this fragment. 76 * <p> 77 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use 78 * {@link #setPreferenceScreen(PreferenceScreen)}. 79 * <p> 80 * As a convenience, this fragment implements a click listener for any 81 * preference in the current hierarchy, see 82 * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. 83 * 84 * <div class="special reference"> 85 * <h3>Developer Guides</h3> 86 * <p>For information about using {@code PreferenceFragment}, 87 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a> 88 * guide.</p> 89 * </div> 90 * 91 * <a name="SampleCode"></a> 92 * <h3>Sample Code</h3> 93 * 94 * <p>The following sample code shows a simple preference fragment that is 95 * populated from a resource. The resource it loads is:</p> 96 * 97 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences} 98 * 99 * <p>The fragment implementation itself simply populates the preferences 100 * when created. Note that the preferences framework takes care of loading 101 * the current values out of the app preferences and writing them when changed:</p> 102 * 103 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java 104 * fragment} 105 * 106 * @see Preference 107 * @see PreferenceScreen 108 */ 109 public abstract class PreferenceFragment extends Fragment implements 110 PreferenceManager.OnPreferenceTreeClickListener { 111 112 private static final String PREFERENCES_TAG = "android:preferences"; 113 114 private PreferenceManager mPreferenceManager; 115 private ListView mList; 116 private boolean mHavePrefs; 117 private boolean mInitDone; 118 119 private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment; 120 121 /** 122 * The starting request code given out to preference framework. 123 */ 124 private static final int FIRST_REQUEST_CODE = 100; 125 126 private static final int MSG_BIND_PREFERENCES = 1; 127 private Handler mHandler = new Handler() { 128 @Override 129 public void handleMessage(Message msg) { 130 switch (msg.what) { 131 132 case MSG_BIND_PREFERENCES: 133 bindPreferences(); 134 break; 135 } 136 } 137 }; 138 139 final private Runnable mRequestFocus = new Runnable() { 140 public void run() { 141 mList.focusableViewAvailable(mList); 142 } 143 }; 144 145 /** 146 * Interface that PreferenceFragment's containing activity should 147 * implement to be able to process preference items that wish to 148 * switch to a new fragment. 149 */ 150 public interface OnPreferenceStartFragmentCallback { 151 /** 152 * Called when the user has clicked on a Preference that has 153 * a fragment class name associated with it. The implementation 154 * to should instantiate and switch to an instance of the given 155 * fragment. 156 */ 157 boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref); 158 } 159 160 @Override 161 public void onCreate(@Nullable Bundle savedInstanceState) { 162 super.onCreate(savedInstanceState); 163 mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE); 164 mPreferenceManager.setFragment(this); 165 } 166 167 @Override 168 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 169 @Nullable Bundle savedInstanceState) { 170 171 TypedArray a = getActivity().obtainStyledAttributes(null, 172 com.android.internal.R.styleable.PreferenceFragment, 173 com.android.internal.R.attr.preferenceFragmentStyle, 174 0); 175 176 mLayoutResId = a.getResourceId(com.android.internal.R.styleable.PreferenceFragment_layout, 177 mLayoutResId); 178 179 a.recycle(); 180 181 return inflater.inflate(mLayoutResId, container, false); 182 } 183 184 @Override 185 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 186 super.onViewCreated(view, savedInstanceState); 187 188 TypedArray a = getActivity().obtainStyledAttributes(null, 189 com.android.internal.R.styleable.PreferenceFragment, 190 com.android.internal.R.attr.preferenceFragmentStyle, 191 0); 192 193 ListView lv = (ListView) view.findViewById(android.R.id.list); 194 if (lv != null 195 && a.hasValueOrEmpty(com.android.internal.R.styleable.PreferenceFragment_divider)) { 196 lv.setDivider( 197 a.getDrawable(com.android.internal.R.styleable.PreferenceFragment_divider)); 198 } 199 200 a.recycle(); 201 } 202 203 @Override 204 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 205 super.onActivityCreated(savedInstanceState); 206 207 if (mHavePrefs) { 208 bindPreferences(); 209 } 210 211 mInitDone = true; 212 213 if (savedInstanceState != null) { 214 Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); 215 if (container != null) { 216 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 217 if (preferenceScreen != null) { 218 preferenceScreen.restoreHierarchyState(container); 219 } 220 } 221 } 222 } 223 224 @Override 225 public void onStart() { 226 super.onStart(); 227 mPreferenceManager.setOnPreferenceTreeClickListener(this); 228 } 229 230 @Override 231 public void onStop() { 232 super.onStop(); 233 mPreferenceManager.dispatchActivityStop(); 234 mPreferenceManager.setOnPreferenceTreeClickListener(null); 235 } 236 237 @Override 238 public void onDestroyView() { 239 if (mList != null) { 240 mList.setOnKeyListener(null); 241 } 242 mList = null; 243 mHandler.removeCallbacks(mRequestFocus); 244 mHandler.removeMessages(MSG_BIND_PREFERENCES); 245 super.onDestroyView(); 246 } 247 248 @Override 249 public void onDestroy() { 250 super.onDestroy(); 251 mPreferenceManager.dispatchActivityDestroy(); 252 } 253 254 @Override 255 public void onSaveInstanceState(Bundle outState) { 256 super.onSaveInstanceState(outState); 257 258 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 259 if (preferenceScreen != null) { 260 Bundle container = new Bundle(); 261 preferenceScreen.saveHierarchyState(container); 262 outState.putBundle(PREFERENCES_TAG, container); 263 } 264 } 265 266 @Override 267 public void onActivityResult(int requestCode, int resultCode, Intent data) { 268 super.onActivityResult(requestCode, resultCode, data); 269 270 mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); 271 } 272 273 /** 274 * Returns the {@link PreferenceManager} used by this fragment. 275 * @return The {@link PreferenceManager}. 276 */ 277 public PreferenceManager getPreferenceManager() { 278 return mPreferenceManager; 279 } 280 281 /** 282 * Sets the root of the preference hierarchy that this fragment is showing. 283 * 284 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 285 */ 286 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 287 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 288 onUnbindPreferences(); 289 mHavePrefs = true; 290 if (mInitDone) { 291 postBindPreferences(); 292 } 293 } 294 } 295 296 /** 297 * Gets the root of the preference hierarchy that this fragment is showing. 298 * 299 * @return The {@link PreferenceScreen} that is the root of the preference 300 * hierarchy. 301 */ 302 public PreferenceScreen getPreferenceScreen() { 303 return mPreferenceManager.getPreferenceScreen(); 304 } 305 306 /** 307 * Adds preferences from activities that match the given {@link Intent}. 308 * 309 * @param intent The {@link Intent} to query activities. 310 */ 311 public void addPreferencesFromIntent(Intent intent) { 312 requirePreferenceManager(); 313 314 setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); 315 } 316 317 /** 318 * Inflates the given XML resource and adds the preference hierarchy to the current 319 * preference hierarchy. 320 * 321 * @param preferencesResId The XML resource ID to inflate. 322 */ 323 public void addPreferencesFromResource(@XmlRes int preferencesResId) { 324 requirePreferenceManager(); 325 326 setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(), 327 preferencesResId, getPreferenceScreen())); 328 } 329 330 /** 331 * {@inheritDoc} 332 */ 333 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, 334 Preference preference) { 335 if (preference.getFragment() != null && 336 getActivity() instanceof OnPreferenceStartFragmentCallback) { 337 return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment( 338 this, preference); 339 } 340 return false; 341 } 342 343 /** 344 * Finds a {@link Preference} based on its key. 345 * 346 * @param key The key of the preference to retrieve. 347 * @return The {@link Preference} with the key, or null. 348 * @see PreferenceGroup#findPreference(CharSequence) 349 */ 350 public Preference findPreference(CharSequence key) { 351 if (mPreferenceManager == null) { 352 return null; 353 } 354 return mPreferenceManager.findPreference(key); 355 } 356 357 private void requirePreferenceManager() { 358 if (mPreferenceManager == null) { 359 throw new RuntimeException("This should be called after super.onCreate."); 360 } 361 } 362 363 private void postBindPreferences() { 364 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 365 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 366 } 367 368 private void bindPreferences() { 369 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 370 if (preferenceScreen != null) { 371 View root = getView(); 372 if (root != null) { 373 View titleView = root.findViewById(android.R.id.title); 374 if (titleView instanceof TextView) { 375 CharSequence title = preferenceScreen.getTitle(); 376 if (TextUtils.isEmpty(title)) { 377 titleView.setVisibility(View.GONE); 378 } else { 379 ((TextView) titleView).setText(title); 380 titleView.setVisibility(View.VISIBLE); 381 } 382 } 383 } 384 385 preferenceScreen.bind(getListView()); 386 } 387 onBindPreferences(); 388 } 389 390 /** @hide */ 391 protected void onBindPreferences() { 392 } 393 394 /** @hide */ 395 protected void onUnbindPreferences() { 396 } 397 398 /** @hide */ 399 public ListView getListView() { 400 ensureList(); 401 return mList; 402 } 403 404 /** @hide */ 405 public boolean hasListView() { 406 if (mList != null) { 407 return true; 408 } 409 View root = getView(); 410 if (root == null) { 411 return false; 412 } 413 View rawListView = root.findViewById(android.R.id.list); 414 if (!(rawListView instanceof ListView)) { 415 return false; 416 } 417 mList = (ListView)rawListView; 418 if (mList == null) { 419 return false; 420 } 421 return true; 422 } 423 424 private void ensureList() { 425 if (mList != null) { 426 return; 427 } 428 View root = getView(); 429 if (root == null) { 430 throw new IllegalStateException("Content view not yet created"); 431 } 432 View rawListView = root.findViewById(android.R.id.list); 433 if (!(rawListView instanceof ListView)) { 434 throw new RuntimeException( 435 "Content has view with id attribute 'android.R.id.list' " 436 + "that is not a ListView class"); 437 } 438 mList = (ListView)rawListView; 439 if (mList == null) { 440 throw new RuntimeException( 441 "Your content must have a ListView whose id attribute is " + 442 "'android.R.id.list'"); 443 } 444 mList.setOnKeyListener(mListOnKeyListener); 445 mHandler.post(mRequestFocus); 446 } 447 448 private OnKeyListener mListOnKeyListener = new OnKeyListener() { 449 450 @Override 451 public boolean onKey(View v, int keyCode, KeyEvent event) { 452 Object selectedItem = mList.getSelectedItem(); 453 if (selectedItem instanceof Preference) { 454 View selectedView = mList.getSelectedView(); 455 return ((Preference)selectedItem).onKey( 456 selectedView, keyCode, event); 457 } 458 return false; 459 } 460 461 }; 462 } 463