PreferenceActivity.java revision b1ad5977bc8178b6d350ebe9099daded4c1ef603
1/* 2 * Copyright (C) 2007 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 17package android.preference; 18 19import com.android.internal.util.XmlUtils; 20 21import org.xmlpull.v1.XmlPullParser; 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.app.Fragment; 25import android.app.ListActivity; 26import android.content.Context; 27import android.content.Intent; 28import android.content.res.Configuration; 29import android.content.res.TypedArray; 30import android.content.res.XmlResourceParser; 31import android.graphics.drawable.Drawable; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.Message; 35import android.text.TextUtils; 36import android.util.AttributeSet; 37import android.util.Log; 38import android.util.Xml; 39import android.view.LayoutInflater; 40import android.view.View; 41import android.view.ViewGroup; 42import android.view.View.OnClickListener; 43import android.widget.ArrayAdapter; 44import android.widget.Button; 45import android.widget.ImageView; 46import android.widget.ListView; 47import android.widget.TextView; 48 49import java.io.IOException; 50import java.util.ArrayList; 51import java.util.List; 52 53/** 54 * This is the base class for an activity to show a hierarchy of preferences 55 * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} 56 * this class only allowed the display of a single set of preference; this 57 * functionality should now be found in the new {@link PreferenceFragment} 58 * class. If you are using PreferenceActivity in its old mode, the documentation 59 * there applies to the deprecated APIs here. 60 * 61 * <p>This activity shows one or more headers of preferences, each of with 62 * is associated with a {@link PreferenceFragment} to display the preferences 63 * of that header. The actual layout and display of these associations can 64 * however vary; currently there are two major approaches it may take: 65 * 66 * <ul> 67 * <li>On a small screen it may display only the headers as a single list 68 * when first launched. Selecting one of the header items will re-launch 69 * the activity with it only showing the PreferenceFragment of that header. 70 * <li>On a large screen in may display both the headers and current 71 * PreferenceFragment together as panes. Selecting a header item switches 72 * to showing the correct PreferenceFragment for that item. 73 * </ul> 74 * 75 * <p>Subclasses of PreferenceActivity should implement 76 * {@link #onBuildHeaders} to populate the header list with the desired 77 * items. Doing this implicitly switches the class into its new "headers 78 * + fragments" mode rather than the old style of just showing a single 79 * preferences list. 80 * 81 * <a name="SampleCode"></a> 82 * <h3>Sample Code</h3> 83 * 84 * <p>The following sample code shows a simple preference activity that 85 * has two different sets of preferences. The implementation, consisting 86 * of the activity itself as well as its two preference fragments is:</p> 87 * 88 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java 89 * activity} 90 * 91 * <p>The preference_headers resource describes the headers to be displayed 92 * and the fragments associated with them. It is: 93 * 94 * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers} 95 * 96 * See {@link PreferenceFragment} for information on implementing the 97 * fragments themselves. 98 */ 99public abstract class PreferenceActivity extends ListActivity implements 100 PreferenceManager.OnPreferenceTreeClickListener { 101 private static final String TAG = "PreferenceActivity"; 102 103 private static final String PREFERENCES_TAG = "android:preferences"; 104 105 private static final String EXTRA_PREFS_SHOW_FRAGMENT = ":android:show_fragment"; 106 107 private static final String EXTRA_PREFS_NO_HEADERS = ":android:no_headers"; 108 109 // extras that allow any preference activity to be launched as part of a wizard 110 111 // show Back and Next buttons? takes boolean parameter 112 // Back will then return RESULT_CANCELED and Next RESULT_OK 113 private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; 114 115 // specify custom text for the Back or Next buttons, or cause a button to not appear 116 // at all by setting it to null 117 private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; 118 private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; 119 120 // --- State for new mode when showing a list of headers + prefs fragment 121 122 private final ArrayList<Header> mHeaders = new ArrayList<Header>(); 123 124 private HeaderAdapter mAdapter; 125 126 private View mPrefsContainer; 127 128 private boolean mSinglePane; 129 130 // --- State for old mode when showing a single preference list 131 132 private PreferenceManager mPreferenceManager; 133 134 private Bundle mSavedInstanceState; 135 136 // --- Common state 137 138 private Button mNextButton; 139 140 /** 141 * The starting request code given out to preference framework. 142 */ 143 private static final int FIRST_REQUEST_CODE = 100; 144 145 private static final int MSG_BIND_PREFERENCES = 0; 146 private Handler mHandler = new Handler() { 147 @Override 148 public void handleMessage(Message msg) { 149 switch (msg.what) { 150 151 case MSG_BIND_PREFERENCES: 152 bindPreferences(); 153 break; 154 } 155 } 156 }; 157 158 private class HeaderViewHolder { 159 ImageView icon; 160 TextView title; 161 TextView summary; 162 } 163 164 private class HeaderAdapter extends ArrayAdapter<Header> { 165 private LayoutInflater mInflater; 166 167 public HeaderAdapter(Context context, List<Header> objects) { 168 super(context, 0, objects); 169 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 170 } 171 172 @Override 173 public View getView(int position, View convertView, ViewGroup parent) { 174 HeaderViewHolder holder; 175 View view; 176 177 if (convertView == null) { 178 view = mInflater.inflate(com.android.internal.R.layout.preference_list_item, 179 parent, false); 180 holder = new HeaderViewHolder(); 181 holder.icon = (ImageView)view.findViewById( 182 com.android.internal.R.id.icon); 183 holder.title = (TextView)view.findViewById( 184 com.android.internal.R.id.title); 185 holder.summary = (TextView)view.findViewById( 186 com.android.internal.R.id.summary); 187 view.setTag(holder); 188 } else { 189 view = convertView; 190 holder = (HeaderViewHolder)view.getTag(); 191 } 192 193 Header header = getItem(position); 194 if (header.icon != null) holder.icon.setImageDrawable(header.icon); 195 else if (header.iconRes != 0) holder.icon.setImageResource(header.iconRes); 196 if (header.title != null) holder.title.setText(header.title); 197 if (header.summary != null) holder.summary.setText(header.summary); 198 199 return view; 200 } 201 } 202 203 /** 204 * Description of a single Header item that the user can select. 205 */ 206 public static class Header { 207 /** 208 * Title of the header that is shown to the user. 209 */ 210 CharSequence title; 211 212 /** 213 * Optional summary describing what this header controls. 214 */ 215 CharSequence summary; 216 217 /** 218 * Optional icon resource to show for this header. 219 */ 220 int iconRes; 221 222 /** 223 * Optional icon drawable to show for this header. (If this is non-null, 224 * the iconRes will be ignored.) 225 */ 226 Drawable icon; 227 228 /** 229 * Full class name of the fragment to display when this header is 230 * selected. 231 */ 232 String fragment; 233 } 234 235 @Override 236 protected void onCreate(Bundle savedInstanceState) { 237 super.onCreate(savedInstanceState); 238 239 setContentView(com.android.internal.R.layout.preference_list_content); 240 241 mPrefsContainer = findViewById(com.android.internal.R.id.prefs); 242 boolean hidingHeaders = onIsHidingHeaders(); 243 mSinglePane = hidingHeaders || !onIsMultiPane(); 244 String initialFragment = getIntent().getStringExtra(EXTRA_PREFS_SHOW_FRAGMENT); 245 246 if (initialFragment != null && mSinglePane) { 247 // If we are just showing a fragment, we want to run in 248 // new fragment mode, but don't need to compute and show 249 // the headers. 250 getListView().setVisibility(View.GONE); 251 mPrefsContainer.setVisibility(View.VISIBLE); 252 switchToHeader(initialFragment); 253 254 } else { 255 // We need to try to build the headers. 256 onBuildHeaders(mHeaders); 257 258 // If there are headers, then at this point we need to show 259 // them and, depending on the screen, we may also show in-line 260 // the currently selected preference fragment. 261 if (mHeaders.size() > 0) { 262 mAdapter = new HeaderAdapter(this, mHeaders); 263 setListAdapter(mAdapter); 264 if (!mSinglePane) { 265 mPrefsContainer.setVisibility(View.VISIBLE); 266 switchToHeader(initialFragment != null 267 ? initialFragment : onGetInitialFragment()); 268 } 269 270 // If there are no headers, we are in the old "just show a screen 271 // of preferences" mode. 272 } else { 273 mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); 274 mPreferenceManager.setOnPreferenceTreeClickListener(this); 275 } 276 } 277 278 getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); 279 280 // see if we should show Back/Next buttons 281 Intent intent = getIntent(); 282 if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { 283 284 findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); 285 286 Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); 287 backButton.setOnClickListener(new OnClickListener() { 288 public void onClick(View v) { 289 setResult(RESULT_CANCELED); 290 finish(); 291 } 292 }); 293 mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); 294 mNextButton.setOnClickListener(new OnClickListener() { 295 public void onClick(View v) { 296 setResult(RESULT_OK); 297 finish(); 298 } 299 }); 300 301 // set our various button parameters 302 if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { 303 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); 304 if (TextUtils.isEmpty(buttonText)) { 305 mNextButton.setVisibility(View.GONE); 306 } 307 else { 308 mNextButton.setText(buttonText); 309 } 310 } 311 if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { 312 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); 313 if (TextUtils.isEmpty(buttonText)) { 314 backButton.setVisibility(View.GONE); 315 } 316 else { 317 backButton.setText(buttonText); 318 } 319 } 320 } 321 } 322 323 /** 324 * Called to determine if the activity should run in multi-pane mode. 325 * The default implementation returns true if the screen is large 326 * enough. 327 */ 328 public boolean onIsMultiPane() { 329 Configuration config = getResources().getConfiguration(); 330 if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) 331 == Configuration.SCREENLAYOUT_SIZE_XLARGE 332 && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 333 return true; 334 } 335 return false; 336 } 337 338 /** 339 * Called to determine whether the header list should be hidden. The 340 * default implementation hides the list if the activity is being re-launched 341 * when not in multi-pane mode. 342 */ 343 public boolean onIsHidingHeaders() { 344 return getIntent().getBooleanExtra(EXTRA_PREFS_NO_HEADERS, false); 345 } 346 347 /** 348 * Called to determine the initial fragment to be shown. The default 349 * implementation simply returns the fragment of the first header. 350 */ 351 public String onGetInitialFragment() { 352 return mHeaders.get(0).fragment; 353 } 354 355 /** 356 * Called when the activity needs its list of headers build. By 357 * implementing this and adding at least one item to the list, you 358 * will cause the activity to run in its modern fragment mode. Note 359 * that this function may not always be called; for example, if the 360 * activity has been asked to display a particular fragment without 361 * the header list, there is no need to build the headers. 362 * 363 * <p>Typical implementations will use {@link #loadHeadersFromResource} 364 * to fill in the list from a resource. 365 * 366 * @param target The list in which to place the headers. 367 */ 368 public void onBuildHeaders(List<Header> target) { 369 } 370 371 /** 372 * Parse the given XML file as a header description, adding each 373 * parsed Header into the target list. 374 * 375 * @param resid The XML resource to load and parse. 376 * @param target The list in which the parsed headers should be placed. 377 */ 378 public void loadHeadersFromResource(int resid, List<Header> target) { 379 XmlResourceParser parser = null; 380 try { 381 parser = getResources().getXml(resid); 382 AttributeSet attrs = Xml.asAttributeSet(parser); 383 384 int type; 385 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 386 && type != XmlPullParser.START_TAG) { 387 } 388 389 String nodeName = parser.getName(); 390 if (!"PreferenceHeaders".equals(nodeName)) { 391 throw new RuntimeException( 392 "XML document must start with <PreferenceHeaders> tag; found" 393 + nodeName + " at " + parser.getPositionDescription()); 394 } 395 396 int outerDepth = parser.getDepth(); 397 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 398 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 399 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 400 continue; 401 } 402 403 nodeName = parser.getName(); 404 if ("Header".equals(nodeName)) { 405 Header header = new Header(); 406 407 TypedArray sa = getResources().obtainAttributes(attrs, 408 com.android.internal.R.styleable.PreferenceHeader); 409 header.title = sa.getText( 410 com.android.internal.R.styleable.PreferenceHeader_title); 411 header.summary = sa.getText( 412 com.android.internal.R.styleable.PreferenceHeader_summary); 413 header.iconRes = sa.getResourceId( 414 com.android.internal.R.styleable.PreferenceHeader_icon, 0); 415 header.fragment = sa.getString( 416 com.android.internal.R.styleable.PreferenceHeader_fragment); 417 sa.recycle(); 418 419 target.add(header); 420 421 XmlUtils.skipCurrentTag(parser); 422 } else { 423 XmlUtils.skipCurrentTag(parser); 424 } 425 } 426 427 } catch (XmlPullParserException e) { 428 throw new RuntimeException("Error parsing headers", e); 429 } catch (IOException e) { 430 throw new RuntimeException("Error parsing headers", e); 431 } finally { 432 if (parser != null) parser.close(); 433 } 434 435 } 436 437 @Override 438 protected void onStop() { 439 super.onStop(); 440 441 if (mPreferenceManager != null) { 442 mPreferenceManager.dispatchActivityStop(); 443 } 444 } 445 446 @Override 447 protected void onDestroy() { 448 super.onDestroy(); 449 450 if (mPreferenceManager != null) { 451 mPreferenceManager.dispatchActivityDestroy(); 452 } 453 } 454 455 @Override 456 protected void onSaveInstanceState(Bundle outState) { 457 super.onSaveInstanceState(outState); 458 459 if (mPreferenceManager != null) { 460 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 461 if (preferenceScreen != null) { 462 Bundle container = new Bundle(); 463 preferenceScreen.saveHierarchyState(container); 464 outState.putBundle(PREFERENCES_TAG, container); 465 } 466 } 467 } 468 469 @Override 470 protected void onRestoreInstanceState(Bundle state) { 471 if (mPreferenceManager != null) { 472 Bundle container = state.getBundle(PREFERENCES_TAG); 473 if (container != null) { 474 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 475 if (preferenceScreen != null) { 476 preferenceScreen.restoreHierarchyState(container); 477 mSavedInstanceState = state; 478 return; 479 } 480 } 481 } 482 483 // Only call this if we didn't save the instance state for later. 484 // If we did save it, it will be restored when we bind the adapter. 485 super.onRestoreInstanceState(state); 486 } 487 488 @Override 489 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 490 super.onActivityResult(requestCode, resultCode, data); 491 492 if (mPreferenceManager != null) { 493 mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); 494 } 495 } 496 497 @Override 498 public void onContentChanged() { 499 super.onContentChanged(); 500 501 if (mPreferenceManager != null) { 502 postBindPreferences(); 503 } 504 } 505 506 @Override 507 protected void onListItemClick(ListView l, View v, int position, long id) { 508 super.onListItemClick(l, v, position, id); 509 510 if (mAdapter != null) { 511 onHeaderClick(mHeaders.get(position), position); 512 } 513 } 514 515 /** 516 * Called when the user selects an item in the header list. The default 517 * implementation will call either {@link #startWithFragment(String)} 518 * or {@link #switchToHeader(String)} as appropriate. 519 * 520 * @param header The header that was selected. 521 * @param position The header's position in the list. 522 */ 523 public void onHeaderClick(Header header, int position) { 524 if (mSinglePane) { 525 startWithFragment(header.fragment); 526 } else { 527 switchToHeader(header.fragment); 528 } 529 } 530 531 /** 532 * Start a new instance of this activity, showing only the given 533 * preference fragment. When launched in this mode, the header list 534 * will be hidden and the given preference fragment will be instantiated 535 * and fill the entire activity. 536 * 537 * @param fragmentName The name of the fragment to display. 538 */ 539 public void startWithFragment(String fragmentName) { 540 Intent intent = new Intent(Intent.ACTION_MAIN); 541 intent.setClass(this, getClass()); 542 intent.putExtra(EXTRA_PREFS_SHOW_FRAGMENT, fragmentName); 543 intent.putExtra(EXTRA_PREFS_NO_HEADERS, true); 544 startActivity(intent); 545 } 546 547 /** 548 * When in two-pane mode, switch the fragment pane to show the given 549 * preference fragment. 550 * 551 * @param fragmentName The name of the fragment to display. 552 */ 553 public void switchToHeader(String fragmentName) { 554 Fragment f; 555 try { 556 f = Fragment.instantiate(this, fragmentName); 557 } catch (Exception e) { 558 Log.w(TAG, "Failure instantiating fragment " + fragmentName, e); 559 return; 560 } 561 openFragmentTransaction().replace(com.android.internal.R.id.prefs, f).commit(); 562 } 563 564 /** 565 * Posts a message to bind the preferences to the list view. 566 * <p> 567 * Binding late is preferred as any custom preference types created in 568 * {@link #onCreate(Bundle)} are able to have their views recycled. 569 */ 570 private void postBindPreferences() { 571 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 572 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 573 } 574 575 private void bindPreferences() { 576 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 577 if (preferenceScreen != null) { 578 preferenceScreen.bind(getListView()); 579 if (mSavedInstanceState != null) { 580 super.onRestoreInstanceState(mSavedInstanceState); 581 mSavedInstanceState = null; 582 } 583 } 584 } 585 586 /** 587 * Returns the {@link PreferenceManager} used by this activity. 588 * @return The {@link PreferenceManager}. 589 * 590 * @deprecated This function is not relevant for a modern fragment-based 591 * PreferenceActivity. 592 */ 593 @Deprecated 594 public PreferenceManager getPreferenceManager() { 595 return mPreferenceManager; 596 } 597 598 private void requirePreferenceManager() { 599 if (mPreferenceManager == null) { 600 if (mAdapter == null) { 601 throw new RuntimeException("This should be called after super.onCreate."); 602 } 603 throw new RuntimeException( 604 "Modern two-pane PreferenceActivity requires use of a PreferenceFragment"); 605 } 606 } 607 608 /** 609 * Sets the root of the preference hierarchy that this activity is showing. 610 * 611 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 612 * 613 * @deprecated This function is not relevant for a modern fragment-based 614 * PreferenceActivity. 615 */ 616 @Deprecated 617 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 618 requirePreferenceManager(); 619 620 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 621 postBindPreferences(); 622 CharSequence title = getPreferenceScreen().getTitle(); 623 // Set the title of the activity 624 if (title != null) { 625 setTitle(title); 626 } 627 } 628 } 629 630 /** 631 * Gets the root of the preference hierarchy that this activity is showing. 632 * 633 * @return The {@link PreferenceScreen} that is the root of the preference 634 * hierarchy. 635 * 636 * @deprecated This function is not relevant for a modern fragment-based 637 * PreferenceActivity. 638 */ 639 @Deprecated 640 public PreferenceScreen getPreferenceScreen() { 641 if (mPreferenceManager != null) { 642 return mPreferenceManager.getPreferenceScreen(); 643 } 644 return null; 645 } 646 647 /** 648 * Adds preferences from activities that match the given {@link Intent}. 649 * 650 * @param intent The {@link Intent} to query activities. 651 * 652 * @deprecated This function is not relevant for a modern fragment-based 653 * PreferenceActivity. 654 */ 655 @Deprecated 656 public void addPreferencesFromIntent(Intent intent) { 657 requirePreferenceManager(); 658 659 setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); 660 } 661 662 /** 663 * Inflates the given XML resource and adds the preference hierarchy to the current 664 * preference hierarchy. 665 * 666 * @param preferencesResId The XML resource ID to inflate. 667 * 668 * @deprecated This function is not relevant for a modern fragment-based 669 * PreferenceActivity. 670 */ 671 @Deprecated 672 public void addPreferencesFromResource(int preferencesResId) { 673 requirePreferenceManager(); 674 675 setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, 676 getPreferenceScreen())); 677 } 678 679 /** 680 * {@inheritDoc} 681 * 682 * @deprecated This function is not relevant for a modern fragment-based 683 * PreferenceActivity. 684 */ 685 @Deprecated 686 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { 687 return false; 688 } 689 690 /** 691 * Finds a {@link Preference} based on its key. 692 * 693 * @param key The key of the preference to retrieve. 694 * @return The {@link Preference} with the key, or null. 695 * @see PreferenceGroup#findPreference(CharSequence) 696 * 697 * @deprecated This function is not relevant for a modern fragment-based 698 * PreferenceActivity. 699 */ 700 @Deprecated 701 public Preference findPreference(CharSequence key) { 702 703 if (mPreferenceManager == null) { 704 return null; 705 } 706 707 return mPreferenceManager.findPreference(key); 708 } 709 710 @Override 711 protected void onNewIntent(Intent intent) { 712 if (mPreferenceManager != null) { 713 mPreferenceManager.dispatchNewIntent(intent); 714 } 715 } 716 717 // give subclasses access to the Next button 718 /** @hide */ 719 protected boolean hasNextButton() { 720 return mNextButton != null; 721 } 722 /** @hide */ 723 protected Button getNextButton() { 724 return mNextButton; 725 } 726} 727