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