• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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