PreferenceActivity.java revision ab36acb39941ce981dddda9f9cf4d2d23a56fd26
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.FragmentBreadCrumbs; 26import android.app.FragmentTransaction; 27import android.app.ListActivity; 28import android.content.Context; 29import android.content.Intent; 30import android.content.res.Configuration; 31import android.content.res.TypedArray; 32import android.content.res.XmlResourceParser; 33import android.os.Bundle; 34import android.os.Handler; 35import android.os.Message; 36import android.os.Parcel; 37import android.os.Parcelable; 38import android.text.TextUtils; 39import android.util.AttributeSet; 40import android.util.Xml; 41import android.view.LayoutInflater; 42import android.view.View; 43import android.view.View.OnClickListener; 44import android.view.ViewGroup; 45import android.widget.AbsListView; 46import android.widget.ArrayAdapter; 47import android.widget.Button; 48import android.widget.FrameLayout; 49import android.widget.ImageView; 50import android.widget.ListView; 51import android.widget.TextView; 52 53import java.io.IOException; 54import java.util.ArrayList; 55import java.util.List; 56 57/** 58 * This is the base class for an activity to show a hierarchy of preferences 59 * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} 60 * this class only allowed the display of a single set of preference; this 61 * functionality should now be found in the new {@link PreferenceFragment} 62 * class. If you are using PreferenceActivity in its old mode, the documentation 63 * there applies to the deprecated APIs here. 64 * 65 * <p>This activity shows one or more headers of preferences, each of with 66 * is associated with a {@link PreferenceFragment} to display the preferences 67 * of that header. The actual layout and display of these associations can 68 * however vary; currently there are two major approaches it may take: 69 * 70 * <ul> 71 * <li>On a small screen it may display only the headers as a single list 72 * when first launched. Selecting one of the header items will re-launch 73 * the activity with it only showing the PreferenceFragment of that header. 74 * <li>On a large screen in may display both the headers and current 75 * PreferenceFragment together as panes. Selecting a header item switches 76 * to showing the correct PreferenceFragment for that item. 77 * </ul> 78 * 79 * <p>Subclasses of PreferenceActivity should implement 80 * {@link #onBuildHeaders} to populate the header list with the desired 81 * items. Doing this implicitly switches the class into its new "headers 82 * + fragments" mode rather than the old style of just showing a single 83 * preferences list. 84 * 85 * <a name="SampleCode"></a> 86 * <h3>Sample Code</h3> 87 * 88 * <p>The following sample code shows a simple preference activity that 89 * has two different sets of preferences. The implementation, consisting 90 * of the activity itself as well as its two preference fragments is:</p> 91 * 92 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java 93 * activity} 94 * 95 * <p>The preference_headers resource describes the headers to be displayed 96 * and the fragments associated with them. It is: 97 * 98 * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers} 99 * 100 * <p>The first header is shown by Prefs1Fragment, which populates itself 101 * from the following XML resource:</p> 102 * 103 * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences} 104 * 105 * <p>Note that this XML resource contains a preference screen holding another 106 * fragment, the Prefs1FragmentInner implemented here. This allows the user 107 * to traverse down a hierarchy of preferences; pressing back will pop each 108 * fragment off the stack to return to the previous preferences. 109 * 110 * <p>See {@link PreferenceFragment} for information on implementing the 111 * fragments themselves. 112 */ 113public abstract class PreferenceActivity extends ListActivity implements 114 PreferenceManager.OnPreferenceTreeClickListener, 115 PreferenceFragment.OnPreferenceStartFragmentCallback { 116 private static final String TAG = "PreferenceActivity"; 117 118 // Constants for state save/restore 119 private static final String HEADERS_TAG = ":android:headers"; 120 private static final String CUR_HEADER_TAG = ":android:cur_header"; 121 private static final String PREFERENCES_TAG = ":android:preferences"; 122 123 /** 124 * When starting this activity, the invoking Intent can contain this extra 125 * string to specify which fragment should be initially displayed. 126 */ 127 public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment"; 128 129 /** 130 * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, 131 * this extra can also be specify to supply a Bundle of arguments to pass 132 * to that fragment when it is instantiated during the initial creation 133 * of PreferenceActivity. 134 */ 135 public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args"; 136 137 /** 138 * When starting this activity, the invoking Intent can contain this extra 139 * boolean that the header list should not be displayed. This is most often 140 * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch 141 * the activity to display a specific fragment that the user has navigated 142 * to. 143 */ 144 public static final String EXTRA_NO_HEADERS = ":android:no_headers"; 145 146 private static final String BACK_STACK_PREFS = ":android:prefs"; 147 148 // extras that allow any preference activity to be launched as part of a wizard 149 150 // show Back and Next buttons? takes boolean parameter 151 // Back will then return RESULT_CANCELED and Next RESULT_OK 152 private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; 153 154 // add a Skip button? 155 private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip"; 156 157 // specify custom text for the Back or Next buttons, or cause a button to not appear 158 // at all by setting it to null 159 private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; 160 private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; 161 162 // --- State for new mode when showing a list of headers + prefs fragment 163 164 private final ArrayList<Header> mHeaders = new ArrayList<Header>(); 165 166 private HeaderAdapter mAdapter; 167 168 private FrameLayout mListFooter; 169 170 private View mPrefsContainer; 171 172 private FragmentBreadCrumbs mFragmentBreadCrumbs; 173 174 private boolean mSinglePane; 175 176 private Header mCurHeader; 177 178 // --- State for old mode when showing a single preference list 179 180 private PreferenceManager mPreferenceManager; 181 182 private Bundle mSavedInstanceState; 183 184 // --- Common state 185 186 private Button mNextButton; 187 188 /** 189 * The starting request code given out to preference framework. 190 */ 191 private static final int FIRST_REQUEST_CODE = 100; 192 193 private static final int MSG_BIND_PREFERENCES = 1; 194 private static final int MSG_BUILD_HEADERS = 2; 195 private Handler mHandler = new Handler() { 196 @Override 197 public void handleMessage(Message msg) { 198 switch (msg.what) { 199 case MSG_BIND_PREFERENCES: { 200 bindPreferences(); 201 } break; 202 case MSG_BUILD_HEADERS: { 203 ArrayList<Header> oldHeaders = new ArrayList<Header>(mHeaders); 204 mHeaders.clear(); 205 onBuildHeaders(mHeaders); 206 if (mAdapter != null) { 207 mAdapter.notifyDataSetChanged(); 208 } 209 Header header = onGetNewHeader(); 210 if (header != null && header.fragment != null) { 211 Header mappedHeader = findBestMatchingHeader(header, oldHeaders); 212 if (mappedHeader == null || mCurHeader != mappedHeader) { 213 switchToHeader(header); 214 } 215 } else if (mCurHeader != null) { 216 Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders); 217 if (mappedHeader != null) { 218 setSelectedHeader(mappedHeader); 219 } 220 } 221 } break; 222 } 223 } 224 }; 225 226 private static class HeaderAdapter extends ArrayAdapter<Header> { 227 private static class HeaderViewHolder { 228 ImageView icon; 229 TextView title; 230 TextView summary; 231 } 232 233 private LayoutInflater mInflater; 234 235 public HeaderAdapter(Context context, List<Header> objects) { 236 super(context, 0, objects); 237 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 238 } 239 240 @Override 241 public View getView(int position, View convertView, ViewGroup parent) { 242 HeaderViewHolder holder; 243 View view; 244 245 if (convertView == null) { 246 view = mInflater.inflate(com.android.internal.R.layout.preference_header_item, 247 parent, false); 248 holder = new HeaderViewHolder(); 249 holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon); 250 holder.title = (TextView) view.findViewById(com.android.internal.R.id.title); 251 holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary); 252 view.setTag(holder); 253 } else { 254 view = convertView; 255 holder = (HeaderViewHolder) view.getTag(); 256 } 257 258 // All view fields must be updated every time, because the view may be recycled 259 Header header = getItem(position); 260 holder.icon.setImageResource(header.iconRes); 261 holder.title.setText(header.title); 262 if (TextUtils.isEmpty(header.summary)) { 263 holder.summary.setVisibility(View.GONE); 264 } else { 265 holder.summary.setVisibility(View.VISIBLE); 266 holder.summary.setText(header.summary); 267 } 268 269 return view; 270 } 271 } 272 273 /** 274 * Default value for {@link Header#id Header.id} indicating that no 275 * identifier value is set. All other values (including those below -1) 276 * are valid. 277 */ 278 public static final long HEADER_ID_UNDEFINED = -1; 279 280 /** 281 * Description of a single Header item that the user can select. 282 */ 283 public static final class Header implements Parcelable { 284 /** 285 * Identifier for this header, to correlate with a new list when 286 * it is updated. The default value is 287 * {@link PreferenceActivity#HEADER_ID_UNDEFINED}, meaning no id. 288 * @attr ref android.R.styleable#PreferenceHeader_id 289 */ 290 public long id = HEADER_ID_UNDEFINED; 291 292 /** 293 * Title of the header that is shown to the user. 294 * @attr ref android.R.styleable#PreferenceHeader_title 295 */ 296 public CharSequence title; 297 298 /** 299 * Optional summary describing what this header controls. 300 * @attr ref android.R.styleable#PreferenceHeader_summary 301 */ 302 public CharSequence summary; 303 304 /** 305 * Optional text to show as the title in the bread crumb. 306 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle 307 */ 308 public CharSequence breadCrumbTitle; 309 310 /** 311 * Optional text to show as the short title in the bread crumb. 312 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle 313 */ 314 public CharSequence breadCrumbShortTitle; 315 316 /** 317 * Optional icon resource to show for this header. 318 * @attr ref android.R.styleable#PreferenceHeader_icon 319 */ 320 public int iconRes; 321 322 /** 323 * Full class name of the fragment to display when this header is 324 * selected. 325 * @attr ref android.R.styleable#PreferenceHeader_fragment 326 */ 327 public String fragment; 328 329 /** 330 * Optional arguments to supply to the fragment when it is 331 * instantiated. 332 */ 333 public Bundle fragmentArguments; 334 335 /** 336 * Intent to launch when the preference is selected. 337 */ 338 public Intent intent; 339 340 /** 341 * Optional additional data for use by subclasses of PreferenceActivity. 342 */ 343 public Bundle extras; 344 345 public Header() { 346 } 347 348 @Override 349 public int describeContents() { 350 return 0; 351 } 352 353 @Override 354 public void writeToParcel(Parcel dest, int flags) { 355 dest.writeLong(id); 356 TextUtils.writeToParcel(title, dest, flags); 357 TextUtils.writeToParcel(summary, dest, flags); 358 TextUtils.writeToParcel(breadCrumbTitle, dest, flags); 359 TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags); 360 dest.writeInt(iconRes); 361 dest.writeString(fragment); 362 dest.writeBundle(fragmentArguments); 363 if (intent != null) { 364 dest.writeInt(1); 365 intent.writeToParcel(dest, flags); 366 } else { 367 dest.writeInt(0); 368 } 369 dest.writeBundle(extras); 370 } 371 372 public void readFromParcel(Parcel in) { 373 id = in.readLong(); 374 title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 375 summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 376 breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 377 breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 378 iconRes = in.readInt(); 379 fragment = in.readString(); 380 fragmentArguments = in.readBundle(); 381 if (in.readInt() != 0) { 382 intent = Intent.CREATOR.createFromParcel(in); 383 } 384 extras = in.readBundle(); 385 } 386 387 Header(Parcel in) { 388 readFromParcel(in); 389 } 390 391 public static final Creator<Header> CREATOR = new Creator<Header>() { 392 public Header createFromParcel(Parcel source) { 393 return new Header(source); 394 } 395 public Header[] newArray(int size) { 396 return new Header[size]; 397 } 398 }; 399 } 400 401 @Override 402 protected void onCreate(Bundle savedInstanceState) { 403 super.onCreate(savedInstanceState); 404 405 setContentView(com.android.internal.R.layout.preference_list_content); 406 407 mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer); 408 mPrefsContainer = findViewById(com.android.internal.R.id.prefs); 409 boolean hidingHeaders = onIsHidingHeaders(); 410 mSinglePane = hidingHeaders || !onIsMultiPane(); 411 String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); 412 Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); 413 414 if (savedInstanceState != null) { 415 // We are restarting from a previous saved state; used that to 416 // initialize, instead of starting fresh. 417 ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG); 418 if (headers != null) { 419 mHeaders.addAll(headers); 420 int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG, 421 (int)HEADER_ID_UNDEFINED); 422 if (curHeader >= 0 && curHeader < mHeaders.size()) { 423 setSelectedHeader(mHeaders.get(curHeader)); 424 } 425 } 426 427 } else { 428 if (initialFragment != null && mSinglePane) { 429 // If we are just showing a fragment, we want to run in 430 // new fragment mode, but don't need to compute and show 431 // the headers. 432 switchToHeader(initialFragment, initialArguments); 433 434 } else { 435 // We need to try to build the headers. 436 onBuildHeaders(mHeaders); 437 438 // If there are headers, then at this point we need to show 439 // them and, depending on the screen, we may also show in-line 440 // the currently selected preference fragment. 441 if (mHeaders.size() > 0) { 442 if (!mSinglePane) { 443 if (initialFragment == null) { 444 Header h = onGetInitialHeader(); 445 switchToHeader(h); 446 } else { 447 switchToHeader(initialFragment, initialArguments); 448 } 449 } 450 } 451 } 452 } 453 454 // The default configuration is to only show the list view. Adjust 455 // visibility for other configurations. 456 if (initialFragment != null && mSinglePane) { 457 // Single pane, showing just a prefs fragment. 458 findViewById(com.android.internal.R.id.headers).setVisibility(View.GONE); 459 mPrefsContainer.setVisibility(View.VISIBLE); 460 } else if (mHeaders.size() > 0) { 461 mAdapter = new HeaderAdapter(this, mHeaders); 462 setListAdapter(mAdapter); 463 if (!mSinglePane) { 464 // Multi-pane. 465 getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); 466 if (mCurHeader != null) { 467 setSelectedHeader(mCurHeader); 468 } 469 mPrefsContainer.setVisibility(View.VISIBLE); 470 } 471 } else { 472 // If there are no headers, we are in the old "just show a screen 473 // of preferences" mode. 474 setContentView(com.android.internal.R.layout.preference_list_content_single); 475 mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer); 476 mPrefsContainer = findViewById(com.android.internal.R.id.prefs); 477 mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); 478 mPreferenceManager.setOnPreferenceTreeClickListener(this); 479 } 480 481 getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); 482 483 // see if we should show Back/Next buttons 484 Intent intent = getIntent(); 485 if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { 486 487 findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); 488 489 Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); 490 backButton.setOnClickListener(new OnClickListener() { 491 public void onClick(View v) { 492 setResult(RESULT_CANCELED); 493 finish(); 494 } 495 }); 496 Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button); 497 skipButton.setOnClickListener(new OnClickListener() { 498 public void onClick(View v) { 499 setResult(RESULT_OK); 500 finish(); 501 } 502 }); 503 mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); 504 mNextButton.setOnClickListener(new OnClickListener() { 505 public void onClick(View v) { 506 setResult(RESULT_OK); 507 finish(); 508 } 509 }); 510 511 // set our various button parameters 512 if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { 513 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); 514 if (TextUtils.isEmpty(buttonText)) { 515 mNextButton.setVisibility(View.GONE); 516 } 517 else { 518 mNextButton.setText(buttonText); 519 } 520 } 521 if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { 522 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); 523 if (TextUtils.isEmpty(buttonText)) { 524 backButton.setVisibility(View.GONE); 525 } 526 else { 527 backButton.setText(buttonText); 528 } 529 } 530 if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) { 531 skipButton.setVisibility(View.VISIBLE); 532 } 533 } 534 } 535 536 /** 537 * Returns true if this activity is currently showing the header list. 538 */ 539 public boolean hasHeaders() { 540 return getListView().getVisibility() == View.VISIBLE 541 && mPreferenceManager == null; 542 } 543 544 /** 545 * Returns true if this activity is showing multiple panes -- the headers 546 * and a preference fragment. 547 */ 548 public boolean isMultiPane() { 549 return hasHeaders() && mPrefsContainer.getVisibility() == View.VISIBLE; 550 } 551 552 /** 553 * Called to determine if the activity should run in multi-pane mode. 554 * The default implementation returns true if the screen is large 555 * enough. 556 */ 557 public boolean onIsMultiPane() { 558 Configuration config = getResources().getConfiguration(); 559 if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) 560 == Configuration.SCREENLAYOUT_SIZE_XLARGE) { 561 return true; 562 } 563 if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) 564 == Configuration.SCREENLAYOUT_SIZE_LARGE 565 && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 566 return true; 567 } 568 return false; 569 } 570 571 /** 572 * Called to determine whether the header list should be hidden. 573 * The default implementation returns the 574 * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied. 575 * This is set to false, for example, when the activity is being re-launched 576 * to show a particular preference activity. 577 */ 578 public boolean onIsHidingHeaders() { 579 return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false); 580 } 581 582 /** 583 * Called to determine the initial header to be shown. The default 584 * implementation simply returns the fragment of the first header. Note 585 * that the returned Header object does not actually need to exist in 586 * your header list -- whatever its fragment is will simply be used to 587 * show for the initial UI. 588 */ 589 public Header onGetInitialHeader() { 590 return mHeaders.get(0); 591 } 592 593 /** 594 * Called after the header list has been updated ({@link #onBuildHeaders} 595 * has been called and returned due to {@link #invalidateHeaders()}) to 596 * specify the header that should now be selected. The default implementation 597 * returns null to keep whatever header is currently selected. 598 */ 599 public Header onGetNewHeader() { 600 return null; 601 } 602 603 /** 604 * Called when the activity needs its list of headers build. By 605 * implementing this and adding at least one item to the list, you 606 * will cause the activity to run in its modern fragment mode. Note 607 * that this function may not always be called; for example, if the 608 * activity has been asked to display a particular fragment without 609 * the header list, there is no need to build the headers. 610 * 611 * <p>Typical implementations will use {@link #loadHeadersFromResource} 612 * to fill in the list from a resource. 613 * 614 * @param target The list in which to place the headers. 615 */ 616 public void onBuildHeaders(List<Header> target) { 617 } 618 619 /** 620 * Call when you need to change the headers being displayed. Will result 621 * in onBuildHeaders() later being called to retrieve the new list. 622 */ 623 public void invalidateHeaders() { 624 if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) { 625 mHandler.sendEmptyMessage(MSG_BUILD_HEADERS); 626 } 627 } 628 629 /** 630 * Parse the given XML file as a header description, adding each 631 * parsed Header into the target list. 632 * 633 * @param resid The XML resource to load and parse. 634 * @param target The list in which the parsed headers should be placed. 635 */ 636 public void loadHeadersFromResource(int resid, List<Header> target) { 637 XmlResourceParser parser = null; 638 try { 639 parser = getResources().getXml(resid); 640 AttributeSet attrs = Xml.asAttributeSet(parser); 641 642 int type; 643 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 644 && type != XmlPullParser.START_TAG) { 645 } 646 647 String nodeName = parser.getName(); 648 if (!"preference-headers".equals(nodeName)) { 649 throw new RuntimeException( 650 "XML document must start with <preference-headers> tag; found" 651 + nodeName + " at " + parser.getPositionDescription()); 652 } 653 654 Bundle curBundle = null; 655 656 final int outerDepth = parser.getDepth(); 657 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 658 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 659 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 660 continue; 661 } 662 663 nodeName = parser.getName(); 664 if ("header".equals(nodeName)) { 665 Header header = new Header(); 666 667 TypedArray sa = getResources().obtainAttributes(attrs, 668 com.android.internal.R.styleable.PreferenceHeader); 669 header.id = sa.getResourceId( 670 com.android.internal.R.styleable.PreferenceHeader_id, 671 (int)HEADER_ID_UNDEFINED); 672 header.title = sa.getText( 673 com.android.internal.R.styleable.PreferenceHeader_title); 674 header.summary = sa.getText( 675 com.android.internal.R.styleable.PreferenceHeader_summary); 676 header.breadCrumbTitle = sa.getText( 677 com.android.internal.R.styleable.PreferenceHeader_breadCrumbTitle); 678 header.breadCrumbShortTitle = sa.getText( 679 com.android.internal.R.styleable.PreferenceHeader_breadCrumbShortTitle); 680 header.iconRes = sa.getResourceId( 681 com.android.internal.R.styleable.PreferenceHeader_icon, 0); 682 header.fragment = sa.getString( 683 com.android.internal.R.styleable.PreferenceHeader_fragment); 684 sa.recycle(); 685 686 if (curBundle == null) { 687 curBundle = new Bundle(); 688 } 689 690 final int innerDepth = parser.getDepth(); 691 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 692 && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { 693 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 694 continue; 695 } 696 697 String innerNodeName = parser.getName(); 698 if (innerNodeName.equals("extra")) { 699 getResources().parseBundleExtra("extra", attrs, curBundle); 700 XmlUtils.skipCurrentTag(parser); 701 702 } else if (innerNodeName.equals("intent")) { 703 header.intent = Intent.parseIntent(getResources(), parser, attrs); 704 705 } else { 706 XmlUtils.skipCurrentTag(parser); 707 } 708 } 709 710 if (curBundle.size() > 0) { 711 header.fragmentArguments = curBundle; 712 curBundle = null; 713 } 714 715 target.add(header); 716 } else { 717 XmlUtils.skipCurrentTag(parser); 718 } 719 } 720 721 } catch (XmlPullParserException e) { 722 throw new RuntimeException("Error parsing headers", e); 723 } catch (IOException e) { 724 throw new RuntimeException("Error parsing headers", e); 725 } finally { 726 if (parser != null) parser.close(); 727 } 728 729 } 730 731 /** 732 * Set a footer that should be shown at the bottom of the header list. 733 */ 734 public void setListFooter(View view) { 735 mListFooter.removeAllViews(); 736 mListFooter.addView(view, new FrameLayout.LayoutParams( 737 FrameLayout.LayoutParams.MATCH_PARENT, 738 FrameLayout.LayoutParams.WRAP_CONTENT)); 739 } 740 741 @Override 742 protected void onStop() { 743 super.onStop(); 744 745 if (mPreferenceManager != null) { 746 mPreferenceManager.dispatchActivityStop(); 747 } 748 } 749 750 @Override 751 protected void onDestroy() { 752 super.onDestroy(); 753 754 if (mPreferenceManager != null) { 755 mPreferenceManager.dispatchActivityDestroy(); 756 } 757 } 758 759 @Override 760 protected void onSaveInstanceState(Bundle outState) { 761 super.onSaveInstanceState(outState); 762 763 if (mHeaders.size() > 0) { 764 outState.putParcelableArrayList(HEADERS_TAG, mHeaders); 765 if (mCurHeader != null) { 766 int index = mHeaders.indexOf(mCurHeader); 767 if (index >= 0) { 768 outState.putInt(CUR_HEADER_TAG, index); 769 } 770 } 771 } 772 773 if (mPreferenceManager != null) { 774 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 775 if (preferenceScreen != null) { 776 Bundle container = new Bundle(); 777 preferenceScreen.saveHierarchyState(container); 778 outState.putBundle(PREFERENCES_TAG, container); 779 } 780 } 781 } 782 783 @Override 784 protected void onRestoreInstanceState(Bundle state) { 785 if (mPreferenceManager != null) { 786 Bundle container = state.getBundle(PREFERENCES_TAG); 787 if (container != null) { 788 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 789 if (preferenceScreen != null) { 790 preferenceScreen.restoreHierarchyState(container); 791 mSavedInstanceState = state; 792 return; 793 } 794 } 795 } 796 797 // Only call this if we didn't save the instance state for later. 798 // If we did save it, it will be restored when we bind the adapter. 799 super.onRestoreInstanceState(state); 800 } 801 802 @Override 803 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 804 super.onActivityResult(requestCode, resultCode, data); 805 806 if (mPreferenceManager != null) { 807 mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); 808 } 809 } 810 811 @Override 812 public void onContentChanged() { 813 super.onContentChanged(); 814 815 if (mPreferenceManager != null) { 816 postBindPreferences(); 817 } 818 } 819 820 @Override 821 protected void onListItemClick(ListView l, View v, int position, long id) { 822 super.onListItemClick(l, v, position, id); 823 824 if (mAdapter != null) { 825 onHeaderClick(mHeaders.get(position), position); 826 } 827 } 828 829 /** 830 * Called when the user selects an item in the header list. The default 831 * implementation will call either {@link #startWithFragment(String, Bundle, Fragment, int)} 832 * or {@link #switchToHeader(Header)} as appropriate. 833 * 834 * @param header The header that was selected. 835 * @param position The header's position in the list. 836 */ 837 public void onHeaderClick(Header header, int position) { 838 if (header.fragment != null) { 839 if (mSinglePane) { 840 startWithFragment(header.fragment, header.fragmentArguments, null, 0); 841 } else { 842 switchToHeader(header); 843 } 844 } else if (header.intent != null) { 845 startActivity(header.intent); 846 } 847 } 848 849 /** 850 * Start a new instance of this activity, showing only the given 851 * preference fragment. When launched in this mode, the header list 852 * will be hidden and the given preference fragment will be instantiated 853 * and fill the entire activity. 854 * 855 * @param fragmentName The name of the fragment to display. 856 * @param args Optional arguments to supply to the fragment. 857 */ 858 public void startWithFragment(String fragmentName, Bundle args, 859 Fragment resultTo, int resultRequestCode) { 860 Intent intent = new Intent(Intent.ACTION_MAIN); 861 intent.setClass(this, getClass()); 862 intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); 863 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); 864 intent.putExtra(EXTRA_NO_HEADERS, true); 865 if (resultTo == null) { 866 startActivity(intent); 867 } else { 868 resultTo.startActivityForResult(intent, resultRequestCode); 869 } 870 } 871 872 /** 873 * Change the base title of the bread crumbs for the current preferences. 874 * This will normally be called for you. See 875 * {@link android.app.FragmentBreadCrumbs} for more information. 876 */ 877 public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) { 878 if (mFragmentBreadCrumbs == null) { 879 mFragmentBreadCrumbs = new FragmentBreadCrumbs(this); 880 mFragmentBreadCrumbs.setActivity(this); 881 getActionBar().setCustomNavigationMode(mFragmentBreadCrumbs); 882 } 883 mFragmentBreadCrumbs.setTitle(title, shortTitle); 884 } 885 886 void setSelectedHeader(Header header) { 887 mCurHeader = header; 888 int index = mHeaders.indexOf(header); 889 if (index >= 0) { 890 getListView().setItemChecked(index, true); 891 } else { 892 getListView().clearChoices(); 893 } 894 if (header != null) { 895 CharSequence title = header.breadCrumbTitle; 896 if (title == null) title = header.title; 897 if (title == null) title = getTitle(); 898 showBreadCrumbs(title, header.breadCrumbShortTitle); 899 } else { 900 showBreadCrumbs(getTitle(), null); 901 } 902 } 903 904 private void switchToHeaderInner(String fragmentName, Bundle args, int direction) { 905 getFragmentManager().popBackStack(BACK_STACK_PREFS, POP_BACK_STACK_INCLUSIVE); 906 Fragment f = Fragment.instantiate(this, fragmentName, args); 907 FragmentTransaction transaction = getFragmentManager().openTransaction(); 908 transaction.setTransition(direction == 0 ? FragmentTransaction.TRANSIT_NONE 909 : direction > 0 ? FragmentTransaction.TRANSIT_FRAGMENT_NEXT 910 : FragmentTransaction.TRANSIT_FRAGMENT_PREV); 911 transaction.replace(com.android.internal.R.id.prefs, f); 912 transaction.commit(); 913 } 914 915 /** 916 * When in two-pane mode, switch the fragment pane to show the given 917 * preference fragment. 918 * 919 * @param fragmentName The name of the fragment to display. 920 * @param args Optional arguments to supply to the fragment. 921 */ 922 public void switchToHeader(String fragmentName, Bundle args) { 923 setSelectedHeader(null); 924 switchToHeaderInner(fragmentName, args, 0); 925 } 926 927 /** 928 * When in two-pane mode, switch to the fragment pane to show the given 929 * preference fragment. 930 * 931 * @param header The new header to display. 932 */ 933 public void switchToHeader(Header header) { 934 if (mCurHeader == header) { 935 // This is the header we are currently displaying. Just make sure 936 // to pop the stack up to its root state. 937 getFragmentManager().popBackStack(BACK_STACK_PREFS, POP_BACK_STACK_INCLUSIVE); 938 } else { 939 int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader); 940 switchToHeaderInner(header.fragment, header.fragmentArguments, direction); 941 setSelectedHeader(header); 942 } 943 } 944 945 Header findBestMatchingHeader(Header cur, ArrayList<Header> from) { 946 ArrayList<Header> matches = new ArrayList<Header>(); 947 for (int j=0; j<from.size(); j++) { 948 Header oh = from.get(j); 949 if (cur == oh || (cur.id != HEADER_ID_UNDEFINED && cur.id == oh.id)) { 950 // Must be this one. 951 matches.clear(); 952 matches.add(oh); 953 break; 954 } 955 if (cur.fragment != null) { 956 if (cur.fragment.equals(oh.fragment)) { 957 matches.add(oh); 958 } 959 } else if (cur.intent != null) { 960 if (cur.intent.equals(oh.intent)) { 961 matches.add(oh); 962 } 963 } else if (cur.title != null) { 964 if (cur.title.equals(oh.title)) { 965 matches.add(oh); 966 } 967 } 968 } 969 final int NM = matches.size(); 970 if (NM == 1) { 971 return matches.get(0); 972 } else if (NM > 1) { 973 for (int j=0; j<NM; j++) { 974 Header oh = matches.get(j); 975 if (cur.fragmentArguments != null && 976 cur.fragmentArguments.equals(oh.fragmentArguments)) { 977 return oh; 978 } 979 if (cur.extras != null && cur.extras.equals(oh.extras)) { 980 return oh; 981 } 982 if (cur.title != null && cur.title.equals(oh.title)) { 983 return oh; 984 } 985 } 986 } 987 return null; 988 } 989 990 /** 991 * Start a new fragment. 992 * 993 * @param fragment The fragment to start 994 * @param push If true, the current fragment will be pushed onto the back stack. If false, 995 * the current fragment will be replaced. 996 */ 997 public void startPreferenceFragment(Fragment fragment, boolean push) { 998 FragmentTransaction transaction = getFragmentManager().openTransaction(); 999 transaction.replace(com.android.internal.R.id.prefs, fragment); 1000 if (push) { 1001 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 1002 transaction.addToBackStack(BACK_STACK_PREFS); 1003 } else { 1004 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_NEXT); 1005 } 1006 transaction.commit(); 1007 } 1008 1009 /** 1010 * Start a new fragment containing a preference panel. If the prefences 1011 * are being displayed in multi-pane mode, the given fragment class will 1012 * be instantiated and placed in the appropriate pane. If running in 1013 * single-pane mode, a new activity will be launched in which to show the 1014 * fragment. 1015 * 1016 * @param fragmentClass Full name of the class implementing the fragment. 1017 * @param args Any desired arguments to supply to the fragment. 1018 * @param titleRes Optional resource identifier of the title of this 1019 * fragment. 1020 * @param titleText Optional text of the title of this fragment. 1021 * @param resultTo Optional fragment that result data should be sent to. 1022 * If non-null, resultTo.onActivityResult() will be called when this 1023 * preference panel is done. The launched panel must use 1024 * {@link #finishPreferencePanel(Fragment, int, Intent)} when done. 1025 * @param resultRequestCode If resultTo is non-null, this is the caller's 1026 * request code to be received with the resut. 1027 */ 1028 public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, 1029 CharSequence titleText, Fragment resultTo, int resultRequestCode) { 1030 if (mSinglePane) { 1031 startWithFragment(fragmentClass, args, resultTo, resultRequestCode); 1032 } else { 1033 Fragment f = Fragment.instantiate(this, fragmentClass, args); 1034 if (resultTo != null) { 1035 f.setTargetFragment(resultTo, resultRequestCode); 1036 } 1037 FragmentTransaction transaction = getFragmentManager().openTransaction(); 1038 transaction.replace(com.android.internal.R.id.prefs, f); 1039 if (titleRes != 0) { 1040 transaction.setBreadCrumbTitle(titleRes); 1041 } else if (titleText != null) { 1042 transaction.setBreadCrumbTitle(titleText); 1043 } 1044 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 1045 transaction.addToBackStack(BACK_STACK_PREFS); 1046 transaction.commit(); 1047 } 1048 } 1049 1050 /** 1051 * Called by a preference panel fragment to finish itself. 1052 * 1053 * @param caller The fragment that is asking to be finished. 1054 * @param resultCode Optional result code to send back to the original 1055 * launching fragment. 1056 * @param resultData Optional result data to send back to the original 1057 * launching fragment. 1058 */ 1059 public void finishPreferencePanel(Fragment caller, int resultCode, Intent resultData) { 1060 if (mSinglePane) { 1061 setResult(resultCode, resultData); 1062 finish(); 1063 } else { 1064 if (caller != null) { 1065 if (caller.getTargetFragment() != null) { 1066 caller.getTargetFragment().onActivityResult(caller.getTargetRequestCode(), 1067 resultCode, resultData); 1068 } 1069 } 1070 // XXX be smarter about popping the stack. 1071 onBackPressed(); 1072 } 1073 } 1074 1075 @Override 1076 public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { 1077 startPreferencePanel(pref.getFragment(), pref.getExtras(), 0, pref.getTitle(), null, 0); 1078 return true; 1079 } 1080 1081 /** 1082 * Posts a message to bind the preferences to the list view. 1083 * <p> 1084 * Binding late is preferred as any custom preference types created in 1085 * {@link #onCreate(Bundle)} are able to have their views recycled. 1086 */ 1087 private void postBindPreferences() { 1088 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 1089 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 1090 } 1091 1092 private void bindPreferences() { 1093 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 1094 if (preferenceScreen != null) { 1095 preferenceScreen.bind(getListView()); 1096 if (mSavedInstanceState != null) { 1097 super.onRestoreInstanceState(mSavedInstanceState); 1098 mSavedInstanceState = null; 1099 } 1100 } 1101 } 1102 1103 /** 1104 * Returns the {@link PreferenceManager} used by this activity. 1105 * @return The {@link PreferenceManager}. 1106 * 1107 * @deprecated This function is not relevant for a modern fragment-based 1108 * PreferenceActivity. 1109 */ 1110 @Deprecated 1111 public PreferenceManager getPreferenceManager() { 1112 return mPreferenceManager; 1113 } 1114 1115 private void requirePreferenceManager() { 1116 if (mPreferenceManager == null) { 1117 if (mAdapter == null) { 1118 throw new RuntimeException("This should be called after super.onCreate."); 1119 } 1120 throw new RuntimeException( 1121 "Modern two-pane PreferenceActivity requires use of a PreferenceFragment"); 1122 } 1123 } 1124 1125 /** 1126 * Sets the root of the preference hierarchy that this activity is showing. 1127 * 1128 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 1129 * 1130 * @deprecated This function is not relevant for a modern fragment-based 1131 * PreferenceActivity. 1132 */ 1133 @Deprecated 1134 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 1135 requirePreferenceManager(); 1136 1137 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 1138 postBindPreferences(); 1139 CharSequence title = getPreferenceScreen().getTitle(); 1140 // Set the title of the activity 1141 if (title != null) { 1142 setTitle(title); 1143 } 1144 } 1145 } 1146 1147 /** 1148 * Gets the root of the preference hierarchy that this activity is showing. 1149 * 1150 * @return The {@link PreferenceScreen} that is the root of the preference 1151 * hierarchy. 1152 * 1153 * @deprecated This function is not relevant for a modern fragment-based 1154 * PreferenceActivity. 1155 */ 1156 @Deprecated 1157 public PreferenceScreen getPreferenceScreen() { 1158 if (mPreferenceManager != null) { 1159 return mPreferenceManager.getPreferenceScreen(); 1160 } 1161 return null; 1162 } 1163 1164 /** 1165 * Adds preferences from activities that match the given {@link Intent}. 1166 * 1167 * @param intent The {@link Intent} to query activities. 1168 * 1169 * @deprecated This function is not relevant for a modern fragment-based 1170 * PreferenceActivity. 1171 */ 1172 @Deprecated 1173 public void addPreferencesFromIntent(Intent intent) { 1174 requirePreferenceManager(); 1175 1176 setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); 1177 } 1178 1179 /** 1180 * Inflates the given XML resource and adds the preference hierarchy to the current 1181 * preference hierarchy. 1182 * 1183 * @param preferencesResId The XML resource ID to inflate. 1184 * 1185 * @deprecated This function is not relevant for a modern fragment-based 1186 * PreferenceActivity. 1187 */ 1188 @Deprecated 1189 public void addPreferencesFromResource(int preferencesResId) { 1190 requirePreferenceManager(); 1191 1192 setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, 1193 getPreferenceScreen())); 1194 } 1195 1196 /** 1197 * {@inheritDoc} 1198 * 1199 * @deprecated This function is not relevant for a modern fragment-based 1200 * PreferenceActivity. 1201 */ 1202 @Deprecated 1203 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { 1204 return false; 1205 } 1206 1207 /** 1208 * Finds a {@link Preference} based on its key. 1209 * 1210 * @param key The key of the preference to retrieve. 1211 * @return The {@link Preference} with the key, or null. 1212 * @see PreferenceGroup#findPreference(CharSequence) 1213 * 1214 * @deprecated This function is not relevant for a modern fragment-based 1215 * PreferenceActivity. 1216 */ 1217 @Deprecated 1218 public Preference findPreference(CharSequence key) { 1219 1220 if (mPreferenceManager == null) { 1221 return null; 1222 } 1223 1224 return mPreferenceManager.findPreference(key); 1225 } 1226 1227 @Override 1228 protected void onNewIntent(Intent intent) { 1229 if (mPreferenceManager != null) { 1230 mPreferenceManager.dispatchNewIntent(intent); 1231 } 1232 } 1233 1234 // give subclasses access to the Next button 1235 /** @hide */ 1236 protected boolean hasNextButton() { 1237 return mNextButton != null; 1238 } 1239 /** @hide */ 1240 protected Button getNextButton() { 1241 return mNextButton; 1242 } 1243} 1244