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