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 17 package android.preference; 18 19 import android.animation.LayoutTransition; 20 import android.annotation.Nullable; 21 import android.annotation.StringRes; 22 import android.annotation.XmlRes; 23 import android.app.Fragment; 24 import android.app.FragmentBreadCrumbs; 25 import android.app.FragmentManager; 26 import android.app.FragmentTransaction; 27 import android.app.ListActivity; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.content.res.XmlResourceParser; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Message; 36 import android.os.Parcel; 37 import android.os.Parcelable; 38 import android.text.TextUtils; 39 import android.util.AttributeSet; 40 import android.util.TypedValue; 41 import android.util.Xml; 42 import android.view.LayoutInflater; 43 import android.view.MenuItem; 44 import android.view.View; 45 import android.view.View.OnClickListener; 46 import android.view.ViewGroup; 47 import android.widget.AbsListView; 48 import android.widget.ArrayAdapter; 49 import android.widget.BaseAdapter; 50 import android.widget.Button; 51 import android.widget.FrameLayout; 52 import android.widget.ImageView; 53 import android.widget.ListView; 54 import android.widget.TextView; 55 56 import com.android.internal.util.XmlUtils; 57 58 import org.xmlpull.v1.XmlPullParser; 59 import org.xmlpull.v1.XmlPullParserException; 60 61 import java.io.IOException; 62 import java.util.ArrayList; 63 import java.util.List; 64 65 /** 66 * This is the base class for an activity to show a hierarchy of preferences 67 * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} 68 * this class only allowed the display of a single set of preference; this 69 * functionality should now be found in the new {@link PreferenceFragment} 70 * class. If you are using PreferenceActivity in its old mode, the documentation 71 * there applies to the deprecated APIs here. 72 * 73 * <p>This activity shows one or more headers of preferences, each of which 74 * is associated with a {@link PreferenceFragment} to display the preferences 75 * of that header. The actual layout and display of these associations can 76 * however vary; currently there are two major approaches it may take: 77 * 78 * <ul> 79 * <li>On a small screen it may display only the headers as a single list when first launched. 80 * Selecting one of the header items will only show the PreferenceFragment of that header (on 81 * Android N and lower a new Activity is launched). 82 * <li>On a large screen in may display both the headers and current PreferenceFragment together as 83 * panes. Selecting a header item switches to showing the correct PreferenceFragment for that item. 84 * </ul> 85 * 86 * <p>Subclasses of PreferenceActivity should implement 87 * {@link #onBuildHeaders} to populate the header list with the desired 88 * items. Doing this implicitly switches the class into its new "headers 89 * + fragments" mode rather than the old style of just showing a single 90 * preferences list. 91 * 92 * <div class="special reference"> 93 * <h3>Developer Guides</h3> 94 * <p>For information about using {@code PreferenceActivity}, 95 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a> 96 * guide.</p> 97 * </div> 98 * 99 * <a name="SampleCode"></a> 100 * <h3>Sample Code</h3> 101 * 102 * <p>The following sample code shows a simple preference activity that 103 * has two different sets of preferences. The implementation, consisting 104 * of the activity itself as well as its two preference fragments is:</p> 105 * 106 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java 107 * activity} 108 * 109 * <p>The preference_headers resource describes the headers to be displayed 110 * and the fragments associated with them. It is: 111 * 112 * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers} 113 * 114 * <p>The first header is shown by Prefs1Fragment, which populates itself 115 * from the following XML resource:</p> 116 * 117 * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences} 118 * 119 * <p>Note that this XML resource contains a preference screen holding another 120 * fragment, the Prefs1FragmentInner implemented here. This allows the user 121 * to traverse down a hierarchy of preferences; pressing back will pop each 122 * fragment off the stack to return to the previous preferences. 123 * 124 * <p>See {@link PreferenceFragment} for information on implementing the 125 * fragments themselves. 126 */ 127 public abstract class PreferenceActivity extends ListActivity implements 128 PreferenceManager.OnPreferenceTreeClickListener, 129 PreferenceFragment.OnPreferenceStartFragmentCallback { 130 131 private static final String TAG = "PreferenceActivity"; 132 133 // Constants for state save/restore 134 private static final String HEADERS_TAG = ":android:headers"; 135 private static final String CUR_HEADER_TAG = ":android:cur_header"; 136 private static final String PREFERENCES_TAG = ":android:preferences"; 137 138 /** 139 * When starting this activity, the invoking Intent can contain this extra 140 * string to specify which fragment should be initially displayed. 141 * <p/>Starting from Key Lime Pie, when this argument is passed in, the PreferenceActivity 142 * will call isValidFragment() to confirm that the fragment class name is valid for this 143 * activity. 144 */ 145 public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment"; 146 147 /** 148 * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, 149 * this extra can also be specified to supply a Bundle of arguments to pass 150 * to that fragment when it is instantiated during the initial creation 151 * of PreferenceActivity. 152 */ 153 public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args"; 154 155 /** 156 * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, 157 * this extra can also be specify to supply the title to be shown for 158 * that fragment. 159 */ 160 public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":android:show_fragment_title"; 161 162 /** 163 * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, 164 * this extra can also be specify to supply the short title to be shown for 165 * that fragment. 166 */ 167 public static final String EXTRA_SHOW_FRAGMENT_SHORT_TITLE 168 = ":android:show_fragment_short_title"; 169 170 /** 171 * When starting this activity, the invoking Intent can contain this extra 172 * boolean that the header list should not be displayed. This is most often 173 * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch 174 * the activity to display a specific fragment that the user has navigated 175 * to. 176 */ 177 public static final String EXTRA_NO_HEADERS = ":android:no_headers"; 178 179 private static final String BACK_STACK_PREFS = ":android:prefs"; 180 181 // extras that allow any preference activity to be launched as part of a wizard 182 183 // show Back and Next buttons? takes boolean parameter 184 // Back will then return RESULT_CANCELED and Next RESULT_OK 185 private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; 186 187 // add a Skip button? 188 private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip"; 189 190 // specify custom text for the Back or Next buttons, or cause a button to not appear 191 // at all by setting it to null 192 private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; 193 private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; 194 195 // --- State for new mode when showing a list of headers + prefs fragment 196 197 private final ArrayList<Header> mHeaders = new ArrayList<Header>(); 198 199 private FrameLayout mListFooter; 200 201 private ViewGroup mPrefsContainer; 202 203 // Backup of the original activity title. This is used when navigating back to the headers list 204 // in onBackPress to restore the title. 205 private CharSequence mActivityTitle; 206 207 // Null if in legacy mode. 208 private ViewGroup mHeadersContainer; 209 210 private FragmentBreadCrumbs mFragmentBreadCrumbs; 211 212 private boolean mSinglePane; 213 214 private Header mCurHeader; 215 216 // --- State for old mode when showing a single preference list 217 218 private PreferenceManager mPreferenceManager; 219 220 private Bundle mSavedInstanceState; 221 222 // --- Common state 223 224 private Button mNextButton; 225 226 private int mPreferenceHeaderItemResId = 0; 227 private boolean mPreferenceHeaderRemoveEmptyIcon = false; 228 229 /** 230 * The starting request code given out to preference framework. 231 */ 232 private static final int FIRST_REQUEST_CODE = 100; 233 234 private static final int MSG_BIND_PREFERENCES = 1; 235 private static final int MSG_BUILD_HEADERS = 2; 236 private Handler mHandler = new Handler() { 237 @Override 238 public void handleMessage(Message msg) { 239 switch (msg.what) { 240 case MSG_BIND_PREFERENCES: { 241 bindPreferences(); 242 } break; 243 case MSG_BUILD_HEADERS: { 244 ArrayList<Header> oldHeaders = new ArrayList<Header>(mHeaders); 245 mHeaders.clear(); 246 onBuildHeaders(mHeaders); 247 if (mAdapter instanceof BaseAdapter) { 248 ((BaseAdapter) mAdapter).notifyDataSetChanged(); 249 } 250 Header header = onGetNewHeader(); 251 if (header != null && header.fragment != null) { 252 Header mappedHeader = findBestMatchingHeader(header, oldHeaders); 253 if (mappedHeader == null || mCurHeader != mappedHeader) { 254 switchToHeader(header); 255 } 256 } else if (mCurHeader != null) { 257 Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders); 258 if (mappedHeader != null) { 259 setSelectedHeader(mappedHeader); 260 } 261 } 262 } break; 263 } 264 } 265 }; 266 267 private static class HeaderAdapter extends ArrayAdapter<Header> { 268 private static class HeaderViewHolder { 269 ImageView icon; 270 TextView title; 271 TextView summary; 272 } 273 274 private LayoutInflater mInflater; 275 private int mLayoutResId; 276 private boolean mRemoveIconIfEmpty; 277 HeaderAdapter(Context context, List<Header> objects, int layoutResId, boolean removeIconBehavior)278 public HeaderAdapter(Context context, List<Header> objects, int layoutResId, 279 boolean removeIconBehavior) { 280 super(context, 0, objects); 281 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 282 mLayoutResId = layoutResId; 283 mRemoveIconIfEmpty = removeIconBehavior; 284 } 285 286 @Override getView(int position, View convertView, ViewGroup parent)287 public View getView(int position, View convertView, ViewGroup parent) { 288 HeaderViewHolder holder; 289 View view; 290 291 if (convertView == null) { 292 view = mInflater.inflate(mLayoutResId, parent, false); 293 holder = new HeaderViewHolder(); 294 holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon); 295 holder.title = (TextView) view.findViewById(com.android.internal.R.id.title); 296 holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary); 297 view.setTag(holder); 298 } else { 299 view = convertView; 300 holder = (HeaderViewHolder) view.getTag(); 301 } 302 303 // All view fields must be updated every time, because the view may be recycled 304 Header header = getItem(position); 305 if (mRemoveIconIfEmpty) { 306 if (header.iconRes == 0) { 307 holder.icon.setVisibility(View.GONE); 308 } else { 309 holder.icon.setVisibility(View.VISIBLE); 310 holder.icon.setImageResource(header.iconRes); 311 } 312 } else { 313 holder.icon.setImageResource(header.iconRes); 314 } 315 holder.title.setText(header.getTitle(getContext().getResources())); 316 CharSequence summary = header.getSummary(getContext().getResources()); 317 if (!TextUtils.isEmpty(summary)) { 318 holder.summary.setVisibility(View.VISIBLE); 319 holder.summary.setText(summary); 320 } else { 321 holder.summary.setVisibility(View.GONE); 322 } 323 324 return view; 325 } 326 } 327 328 /** 329 * Default value for {@link Header#id Header.id} indicating that no 330 * identifier value is set. All other values (including those below -1) 331 * are valid. 332 */ 333 public static final long HEADER_ID_UNDEFINED = -1; 334 335 /** 336 * Description of a single Header item that the user can select. 337 */ 338 public static final class Header implements Parcelable { 339 /** 340 * Identifier for this header, to correlate with a new list when 341 * it is updated. The default value is 342 * {@link PreferenceActivity#HEADER_ID_UNDEFINED}, meaning no id. 343 * @attr ref android.R.styleable#PreferenceHeader_id 344 */ 345 public long id = HEADER_ID_UNDEFINED; 346 347 /** 348 * Resource ID of title of the header that is shown to the user. 349 * @attr ref android.R.styleable#PreferenceHeader_title 350 */ 351 @StringRes 352 public int titleRes; 353 354 /** 355 * Title of the header that is shown to the user. 356 * @attr ref android.R.styleable#PreferenceHeader_title 357 */ 358 public CharSequence title; 359 360 /** 361 * Resource ID of optional summary describing what this header controls. 362 * @attr ref android.R.styleable#PreferenceHeader_summary 363 */ 364 @StringRes 365 public int summaryRes; 366 367 /** 368 * Optional summary describing what this header controls. 369 * @attr ref android.R.styleable#PreferenceHeader_summary 370 */ 371 public CharSequence summary; 372 373 /** 374 * Resource ID of optional text to show as the title in the bread crumb. 375 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle 376 */ 377 @StringRes 378 public int breadCrumbTitleRes; 379 380 /** 381 * Optional text to show as the title in the bread crumb. 382 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle 383 */ 384 public CharSequence breadCrumbTitle; 385 386 /** 387 * Resource ID of optional text to show as the short title in the bread crumb. 388 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle 389 */ 390 @StringRes 391 public int breadCrumbShortTitleRes; 392 393 /** 394 * Optional text to show as the short title in the bread crumb. 395 * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle 396 */ 397 public CharSequence breadCrumbShortTitle; 398 399 /** 400 * Optional icon resource to show for this header. 401 * @attr ref android.R.styleable#PreferenceHeader_icon 402 */ 403 public int iconRes; 404 405 /** 406 * Full class name of the fragment to display when this header is 407 * selected. 408 * @attr ref android.R.styleable#PreferenceHeader_fragment 409 */ 410 public String fragment; 411 412 /** 413 * Optional arguments to supply to the fragment when it is 414 * instantiated. 415 */ 416 public Bundle fragmentArguments; 417 418 /** 419 * Intent to launch when the preference is selected. 420 */ 421 public Intent intent; 422 423 /** 424 * Optional additional data for use by subclasses of PreferenceActivity. 425 */ 426 public Bundle extras; 427 Header()428 public Header() { 429 // Empty 430 } 431 432 /** 433 * Return the currently set title. If {@link #titleRes} is set, 434 * this resource is loaded from <var>res</var> and returned. Otherwise 435 * {@link #title} is returned. 436 */ getTitle(Resources res)437 public CharSequence getTitle(Resources res) { 438 if (titleRes != 0) { 439 return res.getText(titleRes); 440 } 441 return title; 442 } 443 444 /** 445 * Return the currently set summary. If {@link #summaryRes} is set, 446 * this resource is loaded from <var>res</var> and returned. Otherwise 447 * {@link #summary} is returned. 448 */ getSummary(Resources res)449 public CharSequence getSummary(Resources res) { 450 if (summaryRes != 0) { 451 return res.getText(summaryRes); 452 } 453 return summary; 454 } 455 456 /** 457 * Return the currently set bread crumb title. If {@link #breadCrumbTitleRes} is set, 458 * this resource is loaded from <var>res</var> and returned. Otherwise 459 * {@link #breadCrumbTitle} is returned. 460 */ getBreadCrumbTitle(Resources res)461 public CharSequence getBreadCrumbTitle(Resources res) { 462 if (breadCrumbTitleRes != 0) { 463 return res.getText(breadCrumbTitleRes); 464 } 465 return breadCrumbTitle; 466 } 467 468 /** 469 * Return the currently set bread crumb short title. If 470 * {@link #breadCrumbShortTitleRes} is set, 471 * this resource is loaded from <var>res</var> and returned. Otherwise 472 * {@link #breadCrumbShortTitle} is returned. 473 */ getBreadCrumbShortTitle(Resources res)474 public CharSequence getBreadCrumbShortTitle(Resources res) { 475 if (breadCrumbShortTitleRes != 0) { 476 return res.getText(breadCrumbShortTitleRes); 477 } 478 return breadCrumbShortTitle; 479 } 480 481 @Override describeContents()482 public int describeContents() { 483 return 0; 484 } 485 486 @Override writeToParcel(Parcel dest, int flags)487 public void writeToParcel(Parcel dest, int flags) { 488 dest.writeLong(id); 489 dest.writeInt(titleRes); 490 TextUtils.writeToParcel(title, dest, flags); 491 dest.writeInt(summaryRes); 492 TextUtils.writeToParcel(summary, dest, flags); 493 dest.writeInt(breadCrumbTitleRes); 494 TextUtils.writeToParcel(breadCrumbTitle, dest, flags); 495 dest.writeInt(breadCrumbShortTitleRes); 496 TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags); 497 dest.writeInt(iconRes); 498 dest.writeString(fragment); 499 dest.writeBundle(fragmentArguments); 500 if (intent != null) { 501 dest.writeInt(1); 502 intent.writeToParcel(dest, flags); 503 } else { 504 dest.writeInt(0); 505 } 506 dest.writeBundle(extras); 507 } 508 readFromParcel(Parcel in)509 public void readFromParcel(Parcel in) { 510 id = in.readLong(); 511 titleRes = in.readInt(); 512 title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 513 summaryRes = in.readInt(); 514 summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 515 breadCrumbTitleRes = in.readInt(); 516 breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 517 breadCrumbShortTitleRes = in.readInt(); 518 breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 519 iconRes = in.readInt(); 520 fragment = in.readString(); 521 fragmentArguments = in.readBundle(); 522 if (in.readInt() != 0) { 523 intent = Intent.CREATOR.createFromParcel(in); 524 } 525 extras = in.readBundle(); 526 } 527 Header(Parcel in)528 Header(Parcel in) { 529 readFromParcel(in); 530 } 531 532 public static final Creator<Header> CREATOR = new Creator<Header>() { 533 public Header createFromParcel(Parcel source) { 534 return new Header(source); 535 } 536 public Header[] newArray(int size) { 537 return new Header[size]; 538 } 539 }; 540 } 541 542 @Override onOptionsItemSelected(MenuItem item)543 public boolean onOptionsItemSelected(MenuItem item) { 544 if (item.getItemId() == android.R.id.home) { 545 // Override home navigation button to call onBackPressed (b/35152749). 546 onBackPressed(); 547 return true; 548 } 549 return super.onOptionsItemSelected(item); 550 } 551 552 @Override onCreate(@ullable Bundle savedInstanceState)553 protected void onCreate(@Nullable Bundle savedInstanceState) { 554 super.onCreate(savedInstanceState); 555 556 // Theming for the PreferenceActivity layout and for the Preference Header(s) layout 557 TypedArray sa = obtainStyledAttributes(null, 558 com.android.internal.R.styleable.PreferenceActivity, 559 com.android.internal.R.attr.preferenceActivityStyle, 560 0); 561 562 final int layoutResId = sa.getResourceId( 563 com.android.internal.R.styleable.PreferenceActivity_layout, 564 com.android.internal.R.layout.preference_list_content); 565 566 mPreferenceHeaderItemResId = sa.getResourceId( 567 com.android.internal.R.styleable.PreferenceActivity_headerLayout, 568 com.android.internal.R.layout.preference_header_item); 569 mPreferenceHeaderRemoveEmptyIcon = sa.getBoolean( 570 com.android.internal.R.styleable.PreferenceActivity_headerRemoveIconIfEmpty, 571 false); 572 573 sa.recycle(); 574 575 setContentView(layoutResId); 576 577 mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer); 578 mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs_frame); 579 mHeadersContainer = (ViewGroup) findViewById(com.android.internal.R.id.headers); 580 boolean hidingHeaders = onIsHidingHeaders(); 581 mSinglePane = hidingHeaders || !onIsMultiPane(); 582 String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); 583 Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); 584 int initialTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, 0); 585 int initialShortTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, 0); 586 mActivityTitle = getTitle(); 587 588 if (savedInstanceState != null) { 589 // We are restarting from a previous saved state; used that to 590 // initialize, instead of starting fresh. 591 ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG); 592 if (headers != null) { 593 mHeaders.addAll(headers); 594 int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG, 595 (int) HEADER_ID_UNDEFINED); 596 if (curHeader >= 0 && curHeader < mHeaders.size()) { 597 setSelectedHeader(mHeaders.get(curHeader)); 598 } else if (!mSinglePane && initialFragment == null) { 599 switchToHeader(onGetInitialHeader()); 600 } 601 } else { 602 // This will for instance hide breadcrumbs for single pane. 603 showBreadCrumbs(getTitle(), null); 604 } 605 } else { 606 if (!onIsHidingHeaders()) { 607 onBuildHeaders(mHeaders); 608 } 609 610 if (initialFragment != null) { 611 switchToHeader(initialFragment, initialArguments); 612 } else if (!mSinglePane && mHeaders.size() > 0) { 613 switchToHeader(onGetInitialHeader()); 614 } 615 } 616 617 if (mHeaders.size() > 0) { 618 setListAdapter(new HeaderAdapter(this, mHeaders, mPreferenceHeaderItemResId, 619 mPreferenceHeaderRemoveEmptyIcon)); 620 if (!mSinglePane) { 621 getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); 622 } 623 } 624 625 if (mSinglePane && initialFragment != null && initialTitle != 0) { 626 CharSequence initialTitleStr = getText(initialTitle); 627 CharSequence initialShortTitleStr = initialShortTitle != 0 628 ? getText(initialShortTitle) : null; 629 showBreadCrumbs(initialTitleStr, initialShortTitleStr); 630 } 631 632 if (mHeaders.size() == 0 && initialFragment == null) { 633 // If there are no headers, we are in the old "just show a screen 634 // of preferences" mode. 635 setContentView(com.android.internal.R.layout.preference_list_content_single); 636 mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer); 637 mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs); 638 mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); 639 mPreferenceManager.setOnPreferenceTreeClickListener(this); 640 mHeadersContainer = null; 641 } else if (mSinglePane) { 642 // Single-pane so one of the header or prefs containers must be hidden. 643 if (initialFragment != null || mCurHeader != null) { 644 mHeadersContainer.setVisibility(View.GONE); 645 } else { 646 mPrefsContainer.setVisibility(View.GONE); 647 } 648 649 // This animates our manual transitions between headers and prefs panel in single-pane. 650 // It also comes last so we don't animate any initial layout changes done above. 651 ViewGroup container = (ViewGroup) findViewById( 652 com.android.internal.R.id.prefs_container); 653 container.setLayoutTransition(new LayoutTransition()); 654 } else { 655 // Multi-pane 656 if (mHeaders.size() > 0 && mCurHeader != null) { 657 setSelectedHeader(mCurHeader); 658 } 659 } 660 661 // see if we should show Back/Next buttons 662 Intent intent = getIntent(); 663 if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { 664 665 findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); 666 667 Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); 668 backButton.setOnClickListener(new OnClickListener() { 669 public void onClick(View v) { 670 setResult(RESULT_CANCELED); 671 finish(); 672 } 673 }); 674 Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button); 675 skipButton.setOnClickListener(new OnClickListener() { 676 public void onClick(View v) { 677 setResult(RESULT_OK); 678 finish(); 679 } 680 }); 681 mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); 682 mNextButton.setOnClickListener(new OnClickListener() { 683 public void onClick(View v) { 684 setResult(RESULT_OK); 685 finish(); 686 } 687 }); 688 689 // set our various button parameters 690 if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { 691 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); 692 if (TextUtils.isEmpty(buttonText)) { 693 mNextButton.setVisibility(View.GONE); 694 } 695 else { 696 mNextButton.setText(buttonText); 697 } 698 } 699 if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { 700 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); 701 if (TextUtils.isEmpty(buttonText)) { 702 backButton.setVisibility(View.GONE); 703 } 704 else { 705 backButton.setText(buttonText); 706 } 707 } 708 if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) { 709 skipButton.setVisibility(View.VISIBLE); 710 } 711 } 712 } 713 714 @Override onBackPressed()715 public void onBackPressed() { 716 if (mCurHeader != null && mSinglePane && getFragmentManager().getBackStackEntryCount() == 0 717 && getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT) == null) { 718 mCurHeader = null; 719 720 mPrefsContainer.setVisibility(View.GONE); 721 mHeadersContainer.setVisibility(View.VISIBLE); 722 if (mActivityTitle != null) { 723 showBreadCrumbs(mActivityTitle, null); 724 } 725 getListView().clearChoices(); 726 } else { 727 super.onBackPressed(); 728 } 729 } 730 731 /** 732 * Returns true if this activity is currently showing the header list. 733 */ hasHeaders()734 public boolean hasHeaders() { 735 return mHeadersContainer != null && mHeadersContainer.getVisibility() == View.VISIBLE; 736 } 737 738 /** 739 * Returns the Header list 740 * @hide 741 */ getHeaders()742 public List<Header> getHeaders() { 743 return mHeaders; 744 } 745 746 /** 747 * Returns true if this activity is showing multiple panes -- the headers 748 * and a preference fragment. 749 */ isMultiPane()750 public boolean isMultiPane() { 751 return !mSinglePane; 752 } 753 754 /** 755 * Called to determine if the activity should run in multi-pane mode. 756 * The default implementation returns true if the screen is large 757 * enough. 758 */ onIsMultiPane()759 public boolean onIsMultiPane() { 760 boolean preferMultiPane = getResources().getBoolean( 761 com.android.internal.R.bool.preferences_prefer_dual_pane); 762 return preferMultiPane; 763 } 764 765 /** 766 * Called to determine whether the header list should be hidden. 767 * The default implementation returns the 768 * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied. 769 * This is set to false, for example, when the activity is being re-launched 770 * to show a particular preference activity. 771 */ onIsHidingHeaders()772 public boolean onIsHidingHeaders() { 773 return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false); 774 } 775 776 /** 777 * Called to determine the initial header to be shown. The default 778 * implementation simply returns the fragment of the first header. Note 779 * that the returned Header object does not actually need to exist in 780 * your header list -- whatever its fragment is will simply be used to 781 * show for the initial UI. 782 */ onGetInitialHeader()783 public Header onGetInitialHeader() { 784 for (int i=0; i<mHeaders.size(); i++) { 785 Header h = mHeaders.get(i); 786 if (h.fragment != null) { 787 return h; 788 } 789 } 790 throw new IllegalStateException("Must have at least one header with a fragment"); 791 } 792 793 /** 794 * Called after the header list has been updated ({@link #onBuildHeaders} 795 * has been called and returned due to {@link #invalidateHeaders()}) to 796 * specify the header that should now be selected. The default implementation 797 * returns null to keep whatever header is currently selected. 798 */ onGetNewHeader()799 public Header onGetNewHeader() { 800 return null; 801 } 802 803 /** 804 * Called when the activity needs its list of headers build. By 805 * implementing this and adding at least one item to the list, you 806 * will cause the activity to run in its modern fragment mode. Note 807 * that this function may not always be called; for example, if the 808 * activity has been asked to display a particular fragment without 809 * the header list, there is no need to build the headers. 810 * 811 * <p>Typical implementations will use {@link #loadHeadersFromResource} 812 * to fill in the list from a resource. 813 * 814 * @param target The list in which to place the headers. 815 */ onBuildHeaders(List<Header> target)816 public void onBuildHeaders(List<Header> target) { 817 // Should be overloaded by subclasses 818 } 819 820 /** 821 * Call when you need to change the headers being displayed. Will result 822 * in onBuildHeaders() later being called to retrieve the new list. 823 */ invalidateHeaders()824 public void invalidateHeaders() { 825 if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) { 826 mHandler.sendEmptyMessage(MSG_BUILD_HEADERS); 827 } 828 } 829 830 /** 831 * Parse the given XML file as a header description, adding each 832 * parsed Header into the target list. 833 * 834 * @param resid The XML resource to load and parse. 835 * @param target The list in which the parsed headers should be placed. 836 */ loadHeadersFromResource(@mlRes int resid, List<Header> target)837 public void loadHeadersFromResource(@XmlRes int resid, List<Header> target) { 838 XmlResourceParser parser = null; 839 try { 840 parser = getResources().getXml(resid); 841 AttributeSet attrs = Xml.asAttributeSet(parser); 842 843 int type; 844 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 845 && type != XmlPullParser.START_TAG) { 846 // Parse next until start tag is found 847 } 848 849 String nodeName = parser.getName(); 850 if (!"preference-headers".equals(nodeName)) { 851 throw new RuntimeException( 852 "XML document must start with <preference-headers> tag; found" 853 + nodeName + " at " + parser.getPositionDescription()); 854 } 855 856 Bundle curBundle = null; 857 858 final int outerDepth = parser.getDepth(); 859 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 860 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 861 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 862 continue; 863 } 864 865 nodeName = parser.getName(); 866 if ("header".equals(nodeName)) { 867 Header header = new Header(); 868 869 TypedArray sa = obtainStyledAttributes( 870 attrs, com.android.internal.R.styleable.PreferenceHeader); 871 header.id = sa.getResourceId( 872 com.android.internal.R.styleable.PreferenceHeader_id, 873 (int)HEADER_ID_UNDEFINED); 874 TypedValue tv = sa.peekValue( 875 com.android.internal.R.styleable.PreferenceHeader_title); 876 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 877 if (tv.resourceId != 0) { 878 header.titleRes = tv.resourceId; 879 } else { 880 header.title = tv.string; 881 } 882 } 883 tv = sa.peekValue( 884 com.android.internal.R.styleable.PreferenceHeader_summary); 885 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 886 if (tv.resourceId != 0) { 887 header.summaryRes = tv.resourceId; 888 } else { 889 header.summary = tv.string; 890 } 891 } 892 tv = sa.peekValue( 893 com.android.internal.R.styleable.PreferenceHeader_breadCrumbTitle); 894 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 895 if (tv.resourceId != 0) { 896 header.breadCrumbTitleRes = tv.resourceId; 897 } else { 898 header.breadCrumbTitle = tv.string; 899 } 900 } 901 tv = sa.peekValue( 902 com.android.internal.R.styleable.PreferenceHeader_breadCrumbShortTitle); 903 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 904 if (tv.resourceId != 0) { 905 header.breadCrumbShortTitleRes = tv.resourceId; 906 } else { 907 header.breadCrumbShortTitle = tv.string; 908 } 909 } 910 header.iconRes = sa.getResourceId( 911 com.android.internal.R.styleable.PreferenceHeader_icon, 0); 912 header.fragment = sa.getString( 913 com.android.internal.R.styleable.PreferenceHeader_fragment); 914 sa.recycle(); 915 916 if (curBundle == null) { 917 curBundle = new Bundle(); 918 } 919 920 final int innerDepth = parser.getDepth(); 921 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 922 && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { 923 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 924 continue; 925 } 926 927 String innerNodeName = parser.getName(); 928 if (innerNodeName.equals("extra")) { 929 getResources().parseBundleExtra("extra", attrs, curBundle); 930 XmlUtils.skipCurrentTag(parser); 931 932 } else if (innerNodeName.equals("intent")) { 933 header.intent = Intent.parseIntent(getResources(), parser, attrs); 934 935 } else { 936 XmlUtils.skipCurrentTag(parser); 937 } 938 } 939 940 if (curBundle.size() > 0) { 941 header.fragmentArguments = curBundle; 942 curBundle = null; 943 } 944 945 target.add(header); 946 } else { 947 XmlUtils.skipCurrentTag(parser); 948 } 949 } 950 951 } catch (XmlPullParserException e) { 952 throw new RuntimeException("Error parsing headers", e); 953 } catch (IOException e) { 954 throw new RuntimeException("Error parsing headers", e); 955 } finally { 956 if (parser != null) parser.close(); 957 } 958 } 959 960 /** 961 * Subclasses should override this method and verify that the given fragment is a valid type 962 * to be attached to this activity. The default implementation returns <code>true</code> for 963 * apps built for <code>android:targetSdkVersion</code> older than 964 * {@link android.os.Build.VERSION_CODES#KITKAT}. For later versions, it will throw an exception. 965 * @param fragmentName the class name of the Fragment about to be attached to this activity. 966 * @return true if the fragment class name is valid for this Activity and false otherwise. 967 */ isValidFragment(String fragmentName)968 protected boolean isValidFragment(String fragmentName) { 969 if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.KITKAT) { 970 throw new RuntimeException( 971 "Subclasses of PreferenceActivity must override isValidFragment(String)" 972 + " to verify that the Fragment class is valid! " 973 + this.getClass().getName() 974 + " has not checked if fragment " + fragmentName + " is valid."); 975 } else { 976 return true; 977 } 978 } 979 980 /** 981 * Set a footer that should be shown at the bottom of the header list. 982 */ setListFooter(View view)983 public void setListFooter(View view) { 984 mListFooter.removeAllViews(); 985 mListFooter.addView(view, new FrameLayout.LayoutParams( 986 FrameLayout.LayoutParams.MATCH_PARENT, 987 FrameLayout.LayoutParams.WRAP_CONTENT)); 988 } 989 990 @Override onStop()991 protected void onStop() { 992 super.onStop(); 993 994 if (mPreferenceManager != null) { 995 mPreferenceManager.dispatchActivityStop(); 996 } 997 } 998 999 @Override onDestroy()1000 protected void onDestroy() { 1001 mHandler.removeMessages(MSG_BIND_PREFERENCES); 1002 mHandler.removeMessages(MSG_BUILD_HEADERS); 1003 super.onDestroy(); 1004 1005 if (mPreferenceManager != null) { 1006 mPreferenceManager.dispatchActivityDestroy(); 1007 } 1008 } 1009 1010 @Override onSaveInstanceState(Bundle outState)1011 protected void onSaveInstanceState(Bundle outState) { 1012 super.onSaveInstanceState(outState); 1013 1014 if (mHeaders.size() > 0) { 1015 outState.putParcelableArrayList(HEADERS_TAG, mHeaders); 1016 if (mCurHeader != null) { 1017 int index = mHeaders.indexOf(mCurHeader); 1018 if (index >= 0) { 1019 outState.putInt(CUR_HEADER_TAG, index); 1020 } 1021 } 1022 } 1023 1024 if (mPreferenceManager != null) { 1025 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 1026 if (preferenceScreen != null) { 1027 Bundle container = new Bundle(); 1028 preferenceScreen.saveHierarchyState(container); 1029 outState.putBundle(PREFERENCES_TAG, container); 1030 } 1031 } 1032 } 1033 1034 @Override onRestoreInstanceState(Bundle state)1035 protected void onRestoreInstanceState(Bundle state) { 1036 if (mPreferenceManager != null) { 1037 Bundle container = state.getBundle(PREFERENCES_TAG); 1038 if (container != null) { 1039 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 1040 if (preferenceScreen != null) { 1041 preferenceScreen.restoreHierarchyState(container); 1042 mSavedInstanceState = state; 1043 return; 1044 } 1045 } 1046 } 1047 1048 // Only call this if we didn't save the instance state for later. 1049 // If we did save it, it will be restored when we bind the adapter. 1050 super.onRestoreInstanceState(state); 1051 1052 if (!mSinglePane) { 1053 // Multi-pane. 1054 if (mCurHeader != null) { 1055 setSelectedHeader(mCurHeader); 1056 } 1057 } 1058 } 1059 1060 @Override onActivityResult(int requestCode, int resultCode, Intent data)1061 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1062 super.onActivityResult(requestCode, resultCode, data); 1063 1064 if (mPreferenceManager != null) { 1065 mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); 1066 } 1067 } 1068 1069 @Override onContentChanged()1070 public void onContentChanged() { 1071 super.onContentChanged(); 1072 1073 if (mPreferenceManager != null) { 1074 postBindPreferences(); 1075 } 1076 } 1077 1078 @Override onListItemClick(ListView l, View v, int position, long id)1079 protected void onListItemClick(ListView l, View v, int position, long id) { 1080 if (!isResumed()) { 1081 return; 1082 } 1083 super.onListItemClick(l, v, position, id); 1084 1085 if (mAdapter != null) { 1086 Object item = mAdapter.getItem(position); 1087 if (item instanceof Header) onHeaderClick((Header) item, position); 1088 } 1089 } 1090 1091 /** 1092 * Called when the user selects an item in the header list. The default 1093 * implementation will call either 1094 * {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} 1095 * or {@link #switchToHeader(Header)} as appropriate. 1096 * 1097 * @param header The header that was selected. 1098 * @param position The header's position in the list. 1099 */ onHeaderClick(Header header, int position)1100 public void onHeaderClick(Header header, int position) { 1101 if (header.fragment != null) { 1102 switchToHeader(header); 1103 } else if (header.intent != null) { 1104 startActivity(header.intent); 1105 } 1106 } 1107 1108 /** 1109 * Called by {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} when 1110 * in single-pane mode, to build an Intent to launch a new activity showing 1111 * the selected fragment. The default implementation constructs an Intent 1112 * that re-launches the current activity with the appropriate arguments to 1113 * display the fragment. 1114 * 1115 * @param fragmentName The name of the fragment to display. 1116 * @param args Optional arguments to supply to the fragment. 1117 * @param titleRes Optional resource ID of title to show for this item. 1118 * @param shortTitleRes Optional resource ID of short title to show for this item. 1119 * @return Returns an Intent that can be launched to display the given 1120 * fragment. 1121 */ onBuildStartFragmentIntent(String fragmentName, Bundle args, @StringRes int titleRes, int shortTitleRes)1122 public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, 1123 @StringRes int titleRes, int shortTitleRes) { 1124 Intent intent = new Intent(Intent.ACTION_MAIN); 1125 intent.setClass(this, getClass()); 1126 intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); 1127 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); 1128 intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, titleRes); 1129 intent.putExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, shortTitleRes); 1130 intent.putExtra(EXTRA_NO_HEADERS, true); 1131 return intent; 1132 } 1133 1134 /** 1135 * Like {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} 1136 * but uses a 0 titleRes. 1137 */ startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode)1138 public void startWithFragment(String fragmentName, Bundle args, 1139 Fragment resultTo, int resultRequestCode) { 1140 startWithFragment(fragmentName, args, resultTo, resultRequestCode, 0, 0); 1141 } 1142 1143 /** 1144 * Start a new instance of this activity, showing only the given 1145 * preference fragment. When launched in this mode, the header list 1146 * will be hidden and the given preference fragment will be instantiated 1147 * and fill the entire activity. 1148 * 1149 * @param fragmentName The name of the fragment to display. 1150 * @param args Optional arguments to supply to the fragment. 1151 * @param resultTo Option fragment that should receive the result of 1152 * the activity launch. 1153 * @param resultRequestCode If resultTo is non-null, this is the request 1154 * code in which to report the result. 1155 * @param titleRes Resource ID of string to display for the title of 1156 * this set of preferences. 1157 * @param shortTitleRes Resource ID of string to display for the short title of 1158 * this set of preferences. 1159 */ startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode, @StringRes int titleRes, @StringRes int shortTitleRes)1160 public void startWithFragment(String fragmentName, Bundle args, 1161 Fragment resultTo, int resultRequestCode, @StringRes int titleRes, 1162 @StringRes int shortTitleRes) { 1163 Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); 1164 if (resultTo == null) { 1165 startActivity(intent); 1166 } else { 1167 resultTo.startActivityForResult(intent, resultRequestCode); 1168 } 1169 } 1170 1171 /** 1172 * Change the base title of the bread crumbs for the current preferences. 1173 * This will normally be called for you. See 1174 * {@link android.app.FragmentBreadCrumbs} for more information. 1175 */ showBreadCrumbs(CharSequence title, CharSequence shortTitle)1176 public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) { 1177 if (mFragmentBreadCrumbs == null) { 1178 View crumbs = findViewById(android.R.id.title); 1179 // For screens with a different kind of title, don't create breadcrumbs. 1180 try { 1181 mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs; 1182 } catch (ClassCastException e) { 1183 setTitle(title); 1184 return; 1185 } 1186 if (mFragmentBreadCrumbs == null) { 1187 if (title != null) { 1188 setTitle(title); 1189 } 1190 return; 1191 } 1192 if (mSinglePane) { 1193 mFragmentBreadCrumbs.setVisibility(View.GONE); 1194 // Hide the breadcrumb section completely for single-pane 1195 View bcSection = findViewById(com.android.internal.R.id.breadcrumb_section); 1196 if (bcSection != null) bcSection.setVisibility(View.GONE); 1197 setTitle(title); 1198 } 1199 mFragmentBreadCrumbs.setMaxVisible(2); 1200 mFragmentBreadCrumbs.setActivity(this); 1201 } 1202 if (mFragmentBreadCrumbs.getVisibility() != View.VISIBLE) { 1203 setTitle(title); 1204 } else { 1205 mFragmentBreadCrumbs.setTitle(title, shortTitle); 1206 mFragmentBreadCrumbs.setParentTitle(null, null, null); 1207 } 1208 } 1209 1210 /** 1211 * Should be called after onCreate to ensure that the breadcrumbs, if any, were created. 1212 * This prepends a title to the fragment breadcrumbs and attaches a listener to any clicks 1213 * on the parent entry. 1214 * @param title the title for the breadcrumb 1215 * @param shortTitle the short title for the breadcrumb 1216 */ setParentTitle(CharSequence title, CharSequence shortTitle, OnClickListener listener)1217 public void setParentTitle(CharSequence title, CharSequence shortTitle, 1218 OnClickListener listener) { 1219 if (mFragmentBreadCrumbs != null) { 1220 mFragmentBreadCrumbs.setParentTitle(title, shortTitle, listener); 1221 } 1222 } 1223 setSelectedHeader(Header header)1224 void setSelectedHeader(Header header) { 1225 mCurHeader = header; 1226 int index = mHeaders.indexOf(header); 1227 if (index >= 0) { 1228 getListView().setItemChecked(index, true); 1229 } else { 1230 getListView().clearChoices(); 1231 } 1232 showBreadCrumbs(header); 1233 } 1234 showBreadCrumbs(Header header)1235 void showBreadCrumbs(Header header) { 1236 if (header != null) { 1237 CharSequence title = header.getBreadCrumbTitle(getResources()); 1238 if (title == null) title = header.getTitle(getResources()); 1239 if (title == null) title = getTitle(); 1240 showBreadCrumbs(title, header.getBreadCrumbShortTitle(getResources())); 1241 } else { 1242 showBreadCrumbs(getTitle(), null); 1243 } 1244 } 1245 switchToHeaderInner(String fragmentName, Bundle args)1246 private void switchToHeaderInner(String fragmentName, Bundle args) { 1247 getFragmentManager().popBackStack(BACK_STACK_PREFS, 1248 FragmentManager.POP_BACK_STACK_INCLUSIVE); 1249 if (!isValidFragment(fragmentName)) { 1250 throw new IllegalArgumentException("Invalid fragment for this activity: " 1251 + fragmentName); 1252 } 1253 1254 Fragment f = Fragment.instantiate(this, fragmentName, args); 1255 FragmentTransaction transaction = getFragmentManager().beginTransaction(); 1256 transaction.setTransition(mSinglePane 1257 ? FragmentTransaction.TRANSIT_NONE 1258 : FragmentTransaction.TRANSIT_FRAGMENT_FADE); 1259 transaction.replace(com.android.internal.R.id.prefs, f); 1260 transaction.commitAllowingStateLoss(); 1261 1262 if (mSinglePane && mPrefsContainer.getVisibility() == View.GONE) { 1263 // We are transitioning from headers to preferences panel in single-pane so we need 1264 // to hide headers and show the prefs container. 1265 mPrefsContainer.setVisibility(View.VISIBLE); 1266 mHeadersContainer.setVisibility(View.GONE); 1267 } 1268 } 1269 1270 /** 1271 * When in two-pane mode, switch the fragment pane to show the given 1272 * preference fragment. 1273 * 1274 * @param fragmentName The name of the fragment to display. 1275 * @param args Optional arguments to supply to the fragment. 1276 */ switchToHeader(String fragmentName, Bundle args)1277 public void switchToHeader(String fragmentName, Bundle args) { 1278 Header selectedHeader = null; 1279 for (int i = 0; i < mHeaders.size(); i++) { 1280 if (fragmentName.equals(mHeaders.get(i).fragment)) { 1281 selectedHeader = mHeaders.get(i); 1282 break; 1283 } 1284 } 1285 setSelectedHeader(selectedHeader); 1286 switchToHeaderInner(fragmentName, args); 1287 } 1288 1289 /** 1290 * When in two-pane mode, switch to the fragment pane to show the given 1291 * preference fragment. 1292 * 1293 * @param header The new header to display. 1294 */ switchToHeader(Header header)1295 public void switchToHeader(Header header) { 1296 if (mCurHeader == header) { 1297 // This is the header we are currently displaying. Just make sure 1298 // to pop the stack up to its root state. 1299 getFragmentManager().popBackStack(BACK_STACK_PREFS, 1300 FragmentManager.POP_BACK_STACK_INCLUSIVE); 1301 } else { 1302 if (header.fragment == null) { 1303 throw new IllegalStateException("can't switch to header that has no fragment"); 1304 } 1305 switchToHeaderInner(header.fragment, header.fragmentArguments); 1306 setSelectedHeader(header); 1307 } 1308 } 1309 findBestMatchingHeader(Header cur, ArrayList<Header> from)1310 Header findBestMatchingHeader(Header cur, ArrayList<Header> from) { 1311 ArrayList<Header> matches = new ArrayList<Header>(); 1312 for (int j=0; j<from.size(); j++) { 1313 Header oh = from.get(j); 1314 if (cur == oh || (cur.id != HEADER_ID_UNDEFINED && cur.id == oh.id)) { 1315 // Must be this one. 1316 matches.clear(); 1317 matches.add(oh); 1318 break; 1319 } 1320 if (cur.fragment != null) { 1321 if (cur.fragment.equals(oh.fragment)) { 1322 matches.add(oh); 1323 } 1324 } else if (cur.intent != null) { 1325 if (cur.intent.equals(oh.intent)) { 1326 matches.add(oh); 1327 } 1328 } else if (cur.title != null) { 1329 if (cur.title.equals(oh.title)) { 1330 matches.add(oh); 1331 } 1332 } 1333 } 1334 final int NM = matches.size(); 1335 if (NM == 1) { 1336 return matches.get(0); 1337 } else if (NM > 1) { 1338 for (int j=0; j<NM; j++) { 1339 Header oh = matches.get(j); 1340 if (cur.fragmentArguments != null && 1341 cur.fragmentArguments.equals(oh.fragmentArguments)) { 1342 return oh; 1343 } 1344 if (cur.extras != null && cur.extras.equals(oh.extras)) { 1345 return oh; 1346 } 1347 if (cur.title != null && cur.title.equals(oh.title)) { 1348 return oh; 1349 } 1350 } 1351 } 1352 return null; 1353 } 1354 1355 /** 1356 * Start a new fragment. 1357 * 1358 * @param fragment The fragment to start 1359 * @param push If true, the current fragment will be pushed onto the back stack. If false, 1360 * the current fragment will be replaced. 1361 */ startPreferenceFragment(Fragment fragment, boolean push)1362 public void startPreferenceFragment(Fragment fragment, boolean push) { 1363 FragmentTransaction transaction = getFragmentManager().beginTransaction(); 1364 transaction.replace(com.android.internal.R.id.prefs, fragment); 1365 if (push) { 1366 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 1367 transaction.addToBackStack(BACK_STACK_PREFS); 1368 } else { 1369 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); 1370 } 1371 transaction.commitAllowingStateLoss(); 1372 } 1373 1374 /** 1375 * Start a new fragment containing a preference panel. If the preferences 1376 * are being displayed in multi-pane mode, the given fragment class will 1377 * be instantiated and placed in the appropriate pane. If running in 1378 * single-pane mode, a new activity will be launched in which to show the 1379 * fragment. 1380 * 1381 * @param fragmentClass Full name of the class implementing the fragment. 1382 * @param args Any desired arguments to supply to the fragment. 1383 * @param titleRes Optional resource identifier of the title of this 1384 * fragment. 1385 * @param titleText Optional text of the title of this fragment. 1386 * @param resultTo Optional fragment that result data should be sent to. 1387 * If non-null, resultTo.onActivityResult() will be called when this 1388 * preference panel is done. The launched panel must use 1389 * {@link #finishPreferencePanel(Fragment, int, Intent)} when done. 1390 * @param resultRequestCode If resultTo is non-null, this is the caller's 1391 * request code to be received with the result. 1392 */ startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode)1393 public void startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, 1394 CharSequence titleText, Fragment resultTo, int resultRequestCode) { 1395 Fragment f = Fragment.instantiate(this, fragmentClass, args); 1396 if (resultTo != null) { 1397 f.setTargetFragment(resultTo, resultRequestCode); 1398 } 1399 FragmentTransaction transaction = getFragmentManager().beginTransaction(); 1400 transaction.replace(com.android.internal.R.id.prefs, f); 1401 if (titleRes != 0) { 1402 transaction.setBreadCrumbTitle(titleRes); 1403 } else if (titleText != null) { 1404 transaction.setBreadCrumbTitle(titleText); 1405 } 1406 transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 1407 transaction.addToBackStack(BACK_STACK_PREFS); 1408 transaction.commitAllowingStateLoss(); 1409 } 1410 1411 /** 1412 * Called by a preference panel fragment to finish itself. 1413 * 1414 * @param caller The fragment that is asking to be finished. 1415 * @param resultCode Optional result code to send back to the original 1416 * launching fragment. 1417 * @param resultData Optional result data to send back to the original 1418 * launching fragment. 1419 */ finishPreferencePanel(Fragment caller, int resultCode, Intent resultData)1420 public void finishPreferencePanel(Fragment caller, int resultCode, Intent resultData) { 1421 // TODO: be smarter about popping the stack. 1422 onBackPressed(); 1423 if (caller != null) { 1424 if (caller.getTargetFragment() != null) { 1425 caller.getTargetFragment().onActivityResult(caller.getTargetRequestCode(), 1426 resultCode, resultData); 1427 } 1428 } 1429 } 1430 1431 @Override onPreferenceStartFragment(PreferenceFragment caller, Preference pref)1432 public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { 1433 startPreferencePanel(pref.getFragment(), pref.getExtras(), pref.getTitleRes(), 1434 pref.getTitle(), null, 0); 1435 return true; 1436 } 1437 1438 /** 1439 * Posts a message to bind the preferences to the list view. 1440 * <p> 1441 * Binding late is preferred as any custom preference types created in 1442 * {@link #onCreate(Bundle)} are able to have their views recycled. 1443 */ postBindPreferences()1444 private void postBindPreferences() { 1445 if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; 1446 mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); 1447 } 1448 bindPreferences()1449 private void bindPreferences() { 1450 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 1451 if (preferenceScreen != null) { 1452 preferenceScreen.bind(getListView()); 1453 if (mSavedInstanceState != null) { 1454 super.onRestoreInstanceState(mSavedInstanceState); 1455 mSavedInstanceState = null; 1456 } 1457 } 1458 } 1459 1460 /** 1461 * Returns the {@link PreferenceManager} used by this activity. 1462 * @return The {@link PreferenceManager}. 1463 * 1464 * @deprecated This function is not relevant for a modern fragment-based 1465 * PreferenceActivity. 1466 */ 1467 @Deprecated getPreferenceManager()1468 public PreferenceManager getPreferenceManager() { 1469 return mPreferenceManager; 1470 } 1471 requirePreferenceManager()1472 private void requirePreferenceManager() { 1473 if (mPreferenceManager == null) { 1474 if (mAdapter == null) { 1475 throw new RuntimeException("This should be called after super.onCreate."); 1476 } 1477 throw new RuntimeException( 1478 "Modern two-pane PreferenceActivity requires use of a PreferenceFragment"); 1479 } 1480 } 1481 1482 /** 1483 * Sets the root of the preference hierarchy that this activity is showing. 1484 * 1485 * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. 1486 * 1487 * @deprecated This function is not relevant for a modern fragment-based 1488 * PreferenceActivity. 1489 */ 1490 @Deprecated setPreferenceScreen(PreferenceScreen preferenceScreen)1491 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 1492 requirePreferenceManager(); 1493 1494 if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { 1495 postBindPreferences(); 1496 CharSequence title = getPreferenceScreen().getTitle(); 1497 // Set the title of the activity 1498 if (title != null) { 1499 setTitle(title); 1500 } 1501 } 1502 } 1503 1504 /** 1505 * Gets the root of the preference hierarchy that this activity is showing. 1506 * 1507 * @return The {@link PreferenceScreen} that is the root of the preference 1508 * hierarchy. 1509 * 1510 * @deprecated This function is not relevant for a modern fragment-based 1511 * PreferenceActivity. 1512 */ 1513 @Deprecated getPreferenceScreen()1514 public PreferenceScreen getPreferenceScreen() { 1515 if (mPreferenceManager != null) { 1516 return mPreferenceManager.getPreferenceScreen(); 1517 } 1518 return null; 1519 } 1520 1521 /** 1522 * Adds preferences from activities that match the given {@link Intent}. 1523 * 1524 * @param intent The {@link Intent} to query activities. 1525 * 1526 * @deprecated This function is not relevant for a modern fragment-based 1527 * PreferenceActivity. 1528 */ 1529 @Deprecated addPreferencesFromIntent(Intent intent)1530 public void addPreferencesFromIntent(Intent intent) { 1531 requirePreferenceManager(); 1532 1533 setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); 1534 } 1535 1536 /** 1537 * Inflates the given XML resource and adds the preference hierarchy to the current 1538 * preference hierarchy. 1539 * 1540 * @param preferencesResId The XML resource ID to inflate. 1541 * 1542 * @deprecated This function is not relevant for a modern fragment-based 1543 * PreferenceActivity. 1544 */ 1545 @Deprecated addPreferencesFromResource(int preferencesResId)1546 public void addPreferencesFromResource(int preferencesResId) { 1547 requirePreferenceManager(); 1548 1549 setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, 1550 getPreferenceScreen())); 1551 } 1552 1553 /** 1554 * {@inheritDoc} 1555 * 1556 * @deprecated This function is not relevant for a modern fragment-based 1557 * PreferenceActivity. 1558 */ 1559 @Deprecated onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference)1560 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { 1561 return false; 1562 } 1563 1564 /** 1565 * Finds a {@link Preference} based on its key. 1566 * 1567 * @param key The key of the preference to retrieve. 1568 * @return The {@link Preference} with the key, or null. 1569 * @see PreferenceGroup#findPreference(CharSequence) 1570 * 1571 * @deprecated This function is not relevant for a modern fragment-based 1572 * PreferenceActivity. 1573 */ 1574 @Deprecated findPreference(CharSequence key)1575 public Preference findPreference(CharSequence key) { 1576 1577 if (mPreferenceManager == null) { 1578 return null; 1579 } 1580 1581 return mPreferenceManager.findPreference(key); 1582 } 1583 1584 @Override onNewIntent(Intent intent)1585 protected void onNewIntent(Intent intent) { 1586 if (mPreferenceManager != null) { 1587 mPreferenceManager.dispatchNewIntent(intent); 1588 } 1589 } 1590 1591 // give subclasses access to the Next button 1592 /** @hide */ hasNextButton()1593 protected boolean hasNextButton() { 1594 return mNextButton != null; 1595 } 1596 /** @hide */ getNextButton()1597 protected Button getNextButton() { 1598 return mNextButton; 1599 } 1600 } 1601