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