PreferenceActivity.java revision d0fa371f276fde32d81c037006941bc93da0bb03
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
17package android.preference;
18
19import com.android.internal.util.XmlUtils;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.app.Fragment;
25import android.app.FragmentTransaction;
26import android.app.ListActivity;
27import android.content.Context;
28import android.content.Intent;
29import android.content.res.Configuration;
30import android.content.res.TypedArray;
31import android.content.res.XmlResourceParser;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.os.Parcel;
36import android.os.Parcelable;
37import android.text.TextUtils;
38import android.util.AttributeSet;
39import android.util.Xml;
40import android.view.LayoutInflater;
41import android.view.View;
42import android.view.View.OnClickListener;
43import android.view.ViewGroup;
44import android.widget.AbsListView;
45import android.widget.ArrayAdapter;
46import android.widget.Button;
47import android.widget.FrameLayout;
48import android.widget.ImageView;
49import android.widget.ListView;
50import android.widget.TextView;
51
52import java.io.IOException;
53import java.util.ArrayList;
54import java.util.List;
55
56/**
57 * This is the base class for an activity to show a hierarchy of preferences
58 * to the user.  Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB}
59 * this class only allowed the display of a single set of preference; this
60 * functionality should now be found in the new {@link PreferenceFragment}
61 * class.  If you are using PreferenceActivity in its old mode, the documentation
62 * there applies to the deprecated APIs here.
63 *
64 * <p>This activity shows one or more headers of preferences, each of with
65 * is associated with a {@link PreferenceFragment} to display the preferences
66 * of that header.  The actual layout and display of these associations can
67 * however vary; currently there are two major approaches it may take:
68 *
69 * <ul>
70 * <li>On a small screen it may display only the headers as a single list
71 * when first launched.  Selecting one of the header items will re-launch
72 * the activity with it only showing the PreferenceFragment of that header.
73 * <li>On a large screen in may display both the headers and current
74 * PreferenceFragment together as panes.  Selecting a header item switches
75 * to showing the correct PreferenceFragment for that item.
76 * </ul>
77 *
78 * <p>Subclasses of PreferenceActivity should implement
79 * {@link #onBuildHeaders} to populate the header list with the desired
80 * items.  Doing this implicitly switches the class into its new "headers
81 * + fragments" mode rather than the old style of just showing a single
82 * preferences list.
83 *
84 * <a name="SampleCode"></a>
85 * <h3>Sample Code</h3>
86 *
87 * <p>The following sample code shows a simple preference activity that
88 * has two different sets of preferences.  The implementation, consisting
89 * of the activity itself as well as its two preference fragments is:</p>
90 *
91 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java
92 *      activity}
93 *
94 * <p>The preference_headers resource describes the headers to be displayed
95 * and the fragments associated with them.  It is:
96 *
97 * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers}
98 *
99 * <p>The first header is shown by Prefs1Fragment, which populates itself
100 * from the following XML resource:</p>
101 *
102 * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences}
103 *
104 * <p>Note that this XML resource contains a preference screen holding another
105 * fragment, the Prefs1FragmentInner implemented here.  This allows the user
106 * to traverse down a hierarchy of preferences; pressing back will pop each
107 * fragment off the stack to return to the previous preferences.
108 *
109 * <p>See {@link PreferenceFragment} for information on implementing the
110 * fragments themselves.
111 */
112public abstract class PreferenceActivity extends ListActivity implements
113        PreferenceManager.OnPreferenceTreeClickListener,
114        PreferenceFragment.OnPreferenceStartFragmentCallback {
115    private static final String TAG = "PreferenceActivity";
116
117    // Constants for state save/restore
118    private static final String HEADERS_TAG = ":android:headers";
119    private static final String CUR_HEADER_TAG = ":android:cur_header";
120    private static final String SINGLE_PANE_TAG = ":android:single_pane";
121    private static final String PREFERENCES_TAG = ":android:preferences";
122
123    /**
124     * When starting this activity, the invoking Intent can contain this extra
125     * string to specify which fragment should be initially displayed.
126     */
127    public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment";
128
129    /**
130     * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
131     * this extra can also be specify to supply a Bundle of arguments to pass
132     * to that fragment when it is instantiated during the initial creation
133     * of PreferenceActivity.
134     */
135    public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args";
136
137    /**
138     * When starting this activity, the invoking Intent can contain this extra
139     * boolean that the header list should not be displayed.  This is most often
140     * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch
141     * the activity to display a specific fragment that the user has navigated
142     * to.
143     */
144    public static final String EXTRA_NO_HEADERS = ":android:no_headers";
145
146    private static final String BACK_STACK_PREFS = ":android:prefs";
147
148    // extras that allow any preference activity to be launched as part of a wizard
149
150    // show Back and Next buttons? takes boolean parameter
151    // Back will then return RESULT_CANCELED and Next RESULT_OK
152    private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar";
153
154    // add a Skip button?
155    private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip";
156
157    // specify custom text for the Back or Next buttons, or cause a button to not appear
158    // at all by setting it to null
159    private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text";
160    private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text";
161
162    // --- State for new mode when showing a list of headers + prefs fragment
163
164    private final ArrayList<Header> mHeaders = new ArrayList<Header>();
165
166    private HeaderAdapter mAdapter;
167
168    private FrameLayout mListFooter;
169
170    private View mPrefsContainer;
171
172    private boolean mSinglePane;
173
174    private Header mCurHeader;
175
176    // --- State for old mode when showing a single preference list
177
178    private PreferenceManager mPreferenceManager;
179
180    private Bundle mSavedInstanceState;
181
182    // --- Common state
183
184    private Button mNextButton;
185
186    /**
187     * The starting request code given out to preference framework.
188     */
189    private static final int FIRST_REQUEST_CODE = 100;
190
191    private static final int MSG_BIND_PREFERENCES = 1;
192    private static final int MSG_BUILD_HEADERS = 2;
193    private Handler mHandler = new Handler() {
194        @Override
195        public void handleMessage(Message msg) {
196            switch (msg.what) {
197                case MSG_BIND_PREFERENCES: {
198                    bindPreferences();
199                } break;
200                case MSG_BUILD_HEADERS: {
201                    ArrayList<Header> oldHeaders = new ArrayList<Header>(mHeaders);
202                    mHeaders.clear();
203                    onBuildHeaders(mHeaders);
204                    mAdapter.notifyDataSetChanged();
205                    Header header = onGetNewHeader();
206                    if (header != null && header.fragment != null) {
207                        Header mappedHeader = findBestMatchingHeader(header, oldHeaders);
208                        if (mappedHeader == null || mCurHeader != mappedHeader) {
209                            switchToHeader(header);
210                        }
211                    } else if (mCurHeader != null) {
212                        Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders);
213                        if (mappedHeader != null) {
214                            setSelectedHeader(mappedHeader);
215                        } else {
216                            switchToHeader(null);
217                        }
218                    }
219                } break;
220            }
221        }
222    };
223
224    private static class HeaderAdapter extends ArrayAdapter<Header> {
225        private static class HeaderViewHolder {
226            ImageView icon;
227            TextView title;
228            TextView summary;
229        }
230
231        private LayoutInflater mInflater;
232
233        public HeaderAdapter(Context context, List<Header> objects) {
234            super(context, 0, objects);
235            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
236        }
237
238        @Override
239        public View getView(int position, View convertView, ViewGroup parent) {
240            HeaderViewHolder holder;
241            View view;
242
243            if (convertView == null) {
244                view = mInflater.inflate(com.android.internal.R.layout.preference_header_item,
245                        parent, false);
246                holder = new HeaderViewHolder();
247                holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
248                holder.title = (TextView) view.findViewById(com.android.internal.R.id.title);
249                holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary);
250                view.setTag(holder);
251            } else {
252                view = convertView;
253                holder = (HeaderViewHolder) view.getTag();
254            }
255
256            // All view fields must be updated every time, because the view may be recycled
257            Header header = getItem(position);
258            holder.icon.setImageResource(header.iconRes);
259            holder.title.setText(header.title);
260            if (TextUtils.isEmpty(header.summary)) {
261                holder.summary.setVisibility(View.GONE);
262            } else {
263                holder.summary.setVisibility(View.VISIBLE);
264                holder.summary.setText(header.summary);
265            }
266
267            return view;
268        }
269    }
270
271    /**
272     * Default value for {@link Header#id Header.id} indicating that no
273     * identifier value is set.  All other values (including those below -1)
274     * are valid.
275     */
276    public static final long HEADER_ID_UNDEFINED = -1;
277
278    /**
279     * Description of a single Header item that the user can select.
280     */
281    public static final class Header implements Parcelable {
282        /**
283         * Identifier for this header, to correlate with a new list when
284         * it is updated.  The default value is
285         * {@link PreferenceActivity#HEADER_ID_UNDEFINED}, meaning no id.
286         * @attr ref android.R.styleable#PreferenceHeader_id
287         */
288        public long id = HEADER_ID_UNDEFINED;
289
290        /**
291         * Title of the header that is shown to the user.
292         * @attr ref android.R.styleable#PreferenceHeader_title
293         */
294        public CharSequence title;
295
296        /**
297         * Optional summary describing what this header controls.
298         * @attr ref android.R.styleable#PreferenceHeader_summary
299         */
300        public CharSequence summary;
301
302        /**
303         * Optional icon resource to show for this header.
304         * @attr ref android.R.styleable#PreferenceHeader_icon
305         */
306        public int iconRes;
307
308        /**
309         * Full class name of the fragment to display when this header is
310         * selected.
311         * @attr ref android.R.styleable#PreferenceHeader_fragment
312         */
313        public String fragment;
314
315        /**
316         * Optional arguments to supply to the fragment when it is
317         * instantiated.
318         */
319        public Bundle fragmentArguments;
320
321        /**
322         * Intent to launch when the preference is selected.
323         */
324        public Intent intent;
325
326        /**
327         * Optional additional data for use by subclasses of PreferenceActivity.
328         */
329        public Bundle extras;
330
331        public Header() {
332        }
333
334        @Override
335        public int describeContents() {
336            return 0;
337        }
338
339        @Override
340        public void writeToParcel(Parcel dest, int flags) {
341            dest.writeLong(id);
342            TextUtils.writeToParcel(title, dest, flags);
343            TextUtils.writeToParcel(summary, dest, flags);
344            dest.writeInt(iconRes);
345            dest.writeString(fragment);
346            dest.writeBundle(fragmentArguments);
347            if (intent != null) {
348                dest.writeInt(1);
349                intent.writeToParcel(dest, flags);
350            } else {
351                dest.writeInt(0);
352            }
353            dest.writeBundle(extras);
354        }
355
356        public void readFromParcel(Parcel in) {
357            id = in.readLong();
358            title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
359            summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
360            iconRes = in.readInt();
361            fragment = in.readString();
362            fragmentArguments = in.readBundle();
363            if (in.readInt() != 0) {
364                intent = Intent.CREATOR.createFromParcel(in);
365            }
366            extras = in.readBundle();
367        }
368
369        Header(Parcel in) {
370            readFromParcel(in);
371        }
372
373        public static final Creator<Header> CREATOR = new Creator<Header>() {
374            public Header createFromParcel(Parcel source) {
375                return new Header(source);
376            }
377            public Header[] newArray(int size) {
378                return new Header[size];
379            }
380        };
381    }
382
383    @Override
384    protected void onCreate(Bundle savedInstanceState) {
385        super.onCreate(savedInstanceState);
386
387        setContentView(com.android.internal.R.layout.preference_list_content);
388
389        mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer);
390        mPrefsContainer = findViewById(com.android.internal.R.id.prefs);
391        boolean hidingHeaders = onIsHidingHeaders();
392        mSinglePane = hidingHeaders || !onIsMultiPane();
393        String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
394        Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
395
396        if (savedInstanceState != null) {
397            // We are restarting from a previous saved state; used that to
398            // initialize, instead of starting fresh.
399            ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG);
400            if (headers != null) {
401                mHeaders.addAll(headers);
402                int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG,
403                        (int)HEADER_ID_UNDEFINED);
404                if (curHeader >= 0 && curHeader < mHeaders.size()) {
405                    setSelectedHeader(mHeaders.get(curHeader));
406                }
407            }
408            mSinglePane = savedInstanceState.getBoolean(SINGLE_PANE_TAG);
409
410        } else {
411            if (initialFragment != null && mSinglePane) {
412                // If we are just showing a fragment, we want to run in
413                // new fragment mode, but don't need to compute and show
414                // the headers.
415                switchToHeader(initialFragment, initialArguments);
416
417            } else {
418                // We need to try to build the headers.
419                onBuildHeaders(mHeaders);
420
421                // If there are headers, then at this point we need to show
422                // them and, depending on the screen, we may also show in-line
423                // the currently selected preference fragment.
424                if (mHeaders.size() > 0) {
425                    if (!mSinglePane) {
426                        if (initialFragment == null) {
427                            Header h = onGetInitialHeader();
428                            switchToHeader(h);
429                        } else {
430                            switchToHeader(initialFragment, initialArguments);
431                        }
432                    }
433                }
434            }
435        }
436
437        // The default configuration is to only show the list view.  Adjust
438        // visibility for other configurations.
439        if (initialFragment != null && mSinglePane) {
440            // Single pane, showing just a prefs fragment.
441            getListView().setVisibility(View.GONE);
442            mPrefsContainer.setVisibility(View.VISIBLE);
443        } else if (mHeaders.size() > 0) {
444            mAdapter = new HeaderAdapter(this, mHeaders);
445            setListAdapter(mAdapter);
446            if (!mSinglePane) {
447                // Multi-pane.
448                getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
449                if (mCurHeader != null) {
450                    setSelectedHeader(mCurHeader);
451                }
452                mPrefsContainer.setVisibility(View.VISIBLE);
453            }
454        } else {
455            // If there are no headers, we are in the old "just show a screen
456            // of preferences" mode.
457            mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
458            mPreferenceManager.setOnPreferenceTreeClickListener(this);
459        }
460
461        getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
462
463        // see if we should show Back/Next buttons
464        Intent intent = getIntent();
465        if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
466
467            findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE);
468
469            Button backButton = (Button)findViewById(com.android.internal.R.id.back_button);
470            backButton.setOnClickListener(new OnClickListener() {
471                public void onClick(View v) {
472                    setResult(RESULT_CANCELED);
473                    finish();
474                }
475            });
476            Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button);
477            skipButton.setOnClickListener(new OnClickListener() {
478                public void onClick(View v) {
479                    setResult(RESULT_OK);
480                    finish();
481                }
482            });
483            mNextButton = (Button)findViewById(com.android.internal.R.id.next_button);
484            mNextButton.setOnClickListener(new OnClickListener() {
485                public void onClick(View v) {
486                    setResult(RESULT_OK);
487                    finish();
488                }
489            });
490
491            // set our various button parameters
492            if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) {
493                String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT);
494                if (TextUtils.isEmpty(buttonText)) {
495                    mNextButton.setVisibility(View.GONE);
496                }
497                else {
498                    mNextButton.setText(buttonText);
499                }
500            }
501            if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) {
502                String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT);
503                if (TextUtils.isEmpty(buttonText)) {
504                    backButton.setVisibility(View.GONE);
505                }
506                else {
507                    backButton.setText(buttonText);
508                }
509            }
510            if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) {
511                skipButton.setVisibility(View.VISIBLE);
512            }
513        }
514    }
515
516    /**
517     * Returns true if this activity is currently showing the header list.
518     */
519    public boolean hasHeaders() {
520        return getListView().getVisibility() == View.VISIBLE
521                && mPreferenceManager == null;
522    }
523
524    /**
525     * Returns true if this activity is showing multiple panes -- the headers
526     * and a preference fragment.
527     */
528    public boolean isMultiPane() {
529        return hasHeaders() && mPrefsContainer.getVisibility() == View.VISIBLE;
530    }
531
532    /**
533     * Called to determine if the activity should run in multi-pane mode.
534     * The default implementation returns true if the screen is large
535     * enough.
536     */
537    public boolean onIsMultiPane() {
538        Configuration config = getResources().getConfiguration();
539        if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK)
540                == Configuration.SCREENLAYOUT_SIZE_XLARGE
541                && config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
542            return true;
543        }
544        return false;
545    }
546
547    /**
548     * Called to determine whether the header list should be hidden.
549     * The default implementation returns the
550     * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied.
551     * This is set to false, for example, when the activity is being re-launched
552     * to show a particular preference activity.
553     */
554    public boolean onIsHidingHeaders() {
555        return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false);
556    }
557
558    /**
559     * Called to determine the initial header to be shown.  The default
560     * implementation simply returns the fragment of the first header.  Note
561     * that the returned Header object does not actually need to exist in
562     * your header list -- whatever its fragment is will simply be used to
563     * show for the initial UI.
564     */
565    public Header onGetInitialHeader() {
566        return mHeaders.get(0);
567    }
568
569    /**
570     * Called after the header list has been updated ({@link #onBuildHeaders}
571     * has been called and returned due to {@link #invalidateHeaders()}) to
572     * specify the header that should now be selected.  The default implementation
573     * returns null to keep whatever header is currently selected.
574     */
575    public Header onGetNewHeader() {
576        return null;
577    }
578
579    /**
580     * Called when the activity needs its list of headers build.  By
581     * implementing this and adding at least one item to the list, you
582     * will cause the activity to run in its modern fragment mode.  Note
583     * that this function may not always be called; for example, if the
584     * activity has been asked to display a particular fragment without
585     * the header list, there is no need to build the headers.
586     *
587     * <p>Typical implementations will use {@link #loadHeadersFromResource}
588     * to fill in the list from a resource.
589     *
590     * @param target The list in which to place the headers.
591     */
592    public void onBuildHeaders(List<Header> target) {
593    }
594
595    /**
596     * Call when you need to change the headers being displayed.  Will result
597     * in onBuildHeaders() later being called to retrieve the new list.
598     */
599    public void invalidateHeaders() {
600        if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) {
601            mHandler.sendEmptyMessage(MSG_BUILD_HEADERS);
602        }
603    }
604
605    /**
606     * Parse the given XML file as a header description, adding each
607     * parsed Header into the target list.
608     *
609     * @param resid The XML resource to load and parse.
610     * @param target The list in which the parsed headers should be placed.
611     */
612    public void loadHeadersFromResource(int resid, List<Header> target) {
613        XmlResourceParser parser = null;
614        try {
615            parser = getResources().getXml(resid);
616            AttributeSet attrs = Xml.asAttributeSet(parser);
617
618            int type;
619            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
620                    && type != XmlPullParser.START_TAG) {
621            }
622
623            String nodeName = parser.getName();
624            if (!"preference-headers".equals(nodeName)) {
625                throw new RuntimeException(
626                        "XML document must start with <preference-headers> tag; found"
627                        + nodeName + " at " + parser.getPositionDescription());
628            }
629
630            Bundle curBundle = null;
631
632            final int outerDepth = parser.getDepth();
633            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
634                   && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
635                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
636                    continue;
637                }
638
639                nodeName = parser.getName();
640                if ("header".equals(nodeName)) {
641                    Header header = new Header();
642
643                    TypedArray sa = getResources().obtainAttributes(attrs,
644                            com.android.internal.R.styleable.PreferenceHeader);
645                    header.id = sa.getInt(
646                            com.android.internal.R.styleable.PreferenceHeader_id,
647                            (int)HEADER_ID_UNDEFINED);
648                    header.title = sa.getText(
649                            com.android.internal.R.styleable.PreferenceHeader_title);
650                    header.summary = sa.getText(
651                            com.android.internal.R.styleable.PreferenceHeader_summary);
652                    header.iconRes = sa.getResourceId(
653                            com.android.internal.R.styleable.PreferenceHeader_icon, 0);
654                    header.fragment = sa.getString(
655                            com.android.internal.R.styleable.PreferenceHeader_fragment);
656                    sa.recycle();
657
658                    if (curBundle == null) {
659                        curBundle = new Bundle();
660                    }
661
662                    final int innerDepth = parser.getDepth();
663                    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
664                           && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
665                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
666                            continue;
667                        }
668
669                        String innerNodeName = parser.getName();
670                        if (innerNodeName.equals("extra")) {
671                            getResources().parseBundleExtra("extra", attrs, curBundle);
672                            XmlUtils.skipCurrentTag(parser);
673
674                        } else if (innerNodeName.equals("intent")) {
675                            header.intent = Intent.parseIntent(getResources(), parser, attrs);
676
677                        } else {
678                            XmlUtils.skipCurrentTag(parser);
679                        }
680                    }
681
682                    if (curBundle.size() > 0) {
683                        header.fragmentArguments = curBundle;
684                        curBundle = null;
685                    }
686
687                    target.add(header);
688                } else {
689                    XmlUtils.skipCurrentTag(parser);
690                }
691            }
692
693        } catch (XmlPullParserException e) {
694            throw new RuntimeException("Error parsing headers", e);
695        } catch (IOException e) {
696            throw new RuntimeException("Error parsing headers", e);
697        } finally {
698            if (parser != null) parser.close();
699        }
700
701    }
702
703    /**
704     * Set a footer that should be shown at the bottom of the header list.
705     */
706    public void setListFooter(View view) {
707        mListFooter.removeAllViews();
708        mListFooter.addView(view, new FrameLayout.LayoutParams(
709                FrameLayout.LayoutParams.MATCH_PARENT,
710                FrameLayout.LayoutParams.WRAP_CONTENT));
711    }
712
713    @Override
714    protected void onStop() {
715        super.onStop();
716
717        if (mPreferenceManager != null) {
718            mPreferenceManager.dispatchActivityStop();
719        }
720    }
721
722    @Override
723    protected void onDestroy() {
724        super.onDestroy();
725
726        if (mPreferenceManager != null) {
727            mPreferenceManager.dispatchActivityDestroy();
728        }
729    }
730
731    @Override
732    protected void onSaveInstanceState(Bundle outState) {
733        super.onSaveInstanceState(outState);
734
735        if (mHeaders.size() > 0) {
736            outState.putParcelableArrayList(HEADERS_TAG, mHeaders);
737            if (mCurHeader != null) {
738                int index = mHeaders.indexOf(mCurHeader);
739                if (index >= 0) {
740                    outState.putInt(CUR_HEADER_TAG, index);
741                }
742            }
743        }
744        outState.putBoolean(SINGLE_PANE_TAG, mSinglePane);
745
746        if (mPreferenceManager != null) {
747            final PreferenceScreen preferenceScreen = getPreferenceScreen();
748            if (preferenceScreen != null) {
749                Bundle container = new Bundle();
750                preferenceScreen.saveHierarchyState(container);
751                outState.putBundle(PREFERENCES_TAG, container);
752            }
753        }
754    }
755
756    @Override
757    protected void onRestoreInstanceState(Bundle state) {
758        if (mPreferenceManager != null) {
759            Bundle container = state.getBundle(PREFERENCES_TAG);
760            if (container != null) {
761                final PreferenceScreen preferenceScreen = getPreferenceScreen();
762                if (preferenceScreen != null) {
763                    preferenceScreen.restoreHierarchyState(container);
764                    mSavedInstanceState = state;
765                    return;
766                }
767            }
768        }
769
770        // Only call this if we didn't save the instance state for later.
771        // If we did save it, it will be restored when we bind the adapter.
772        super.onRestoreInstanceState(state);
773    }
774
775    @Override
776    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
777        super.onActivityResult(requestCode, resultCode, data);
778
779        if (mPreferenceManager != null) {
780            mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
781        }
782    }
783
784    @Override
785    public void onContentChanged() {
786        super.onContentChanged();
787
788        if (mPreferenceManager != null) {
789            postBindPreferences();
790        }
791    }
792
793    @Override
794    protected void onListItemClick(ListView l, View v, int position, long id) {
795        super.onListItemClick(l, v, position, id);
796
797        if (mAdapter != null) {
798            onHeaderClick(mHeaders.get(position), position);
799        }
800    }
801
802    /**
803     * Called when the user selects an item in the header list.  The default
804     * implementation will call either {@link #startWithFragment(String, Bundle)}
805     * or {@link #switchToHeader(Header)} as appropriate.
806     *
807     * @param header The header that was selected.
808     * @param position The header's position in the list.
809     */
810    public void onHeaderClick(Header header, int position) {
811        if (header.fragment != null) {
812            if (mSinglePane) {
813                startWithFragment(header.fragment, header.fragmentArguments);
814            } else {
815                switchToHeader(header);
816            }
817        } else if (header.intent != null) {
818            startActivity(header.intent);
819        }
820    }
821
822    /**
823     * Start a new instance of this activity, showing only the given
824     * preference fragment.  When launched in this mode, the header list
825     * will be hidden and the given preference fragment will be instantiated
826     * and fill the entire activity.
827     *
828     * @param fragmentName The name of the fragment to display.
829     * @param args Optional arguments to supply to the fragment.
830     */
831    public void startWithFragment(String fragmentName, Bundle args) {
832        Intent intent = new Intent(Intent.ACTION_MAIN);
833        intent.setClass(this, getClass());
834        intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName);
835        intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
836        intent.putExtra(EXTRA_NO_HEADERS, true);
837        startActivity(intent);
838    }
839
840    void setSelectedHeader(Header header) {
841        mCurHeader = header;
842        int index = mHeaders.indexOf(header);
843        if (index >= 0) {
844            getListView().setItemChecked(index, true);
845        } else {
846            getListView().clearChoices();
847        }
848    }
849
850    /**
851     * When in two-pane mode, switch the fragment pane to show the given
852     * preference fragment.
853     *
854     * @param fragmentName The name of the fragment to display.
855     * @param args Optional arguments to supply to the fragment.
856     */
857    public void switchToHeader(String fragmentName, Bundle args) {
858        setSelectedHeader(null);
859
860        getFragmentManager().popBackStack(BACK_STACK_PREFS, POP_BACK_STACK_INCLUSIVE);
861
862        Fragment f = Fragment.instantiate(this, fragmentName, args);
863        getFragmentManager().openTransaction().replace(
864                com.android.internal.R.id.prefs, f).commit();
865    }
866
867    /**
868     * When in two-pane mode, switch to the fragment pane to show the given
869     * preference fragment.
870     *
871     * @param header The new header to display.
872     */
873    public void switchToHeader(Header header) {
874        switchToHeader(header.fragment, header.fragmentArguments);
875        mCurHeader = header;
876        setSelectedHeader(header);
877    }
878
879    Header findBestMatchingHeader(Header cur, ArrayList<Header> from) {
880        ArrayList<Header> matches = new ArrayList<Header>();
881        for (int j=0; j<from.size(); j++) {
882            Header oh = from.get(j);
883            if (cur == oh || (cur.id != HEADER_ID_UNDEFINED && cur.id == oh.id)) {
884                // Must be this one.
885                matches.clear();
886                matches.add(oh);
887                break;
888            }
889            if (cur.fragment != null) {
890                if (cur.fragment.equals(oh.fragment)) {
891                    matches.add(oh);
892                }
893            } else if (cur.intent != null) {
894                if (cur.intent.equals(oh.intent)) {
895                    matches.add(oh);
896                }
897            } else if (cur.title != null) {
898                if (cur.title.equals(oh.title)) {
899                    matches.add(oh);
900                }
901            }
902        }
903        final int NM = matches.size();
904        if (NM == 1) {
905            return matches.get(0);
906        } else if (NM > 1) {
907            for (int j=0; j<NM; j++) {
908                Header oh = matches.get(j);
909                if (cur.fragmentArguments != null &&
910                        cur.fragmentArguments.equals(oh.fragmentArguments)) {
911                    return oh;
912                }
913                if (cur.extras != null && cur.extras.equals(oh.extras)) {
914                    return oh;
915                }
916                if (cur.title != null && cur.title.equals(oh.title)) {
917                    return oh;
918                }
919            }
920        }
921        return null;
922    }
923
924    /**
925     * Start a new fragment.
926     *
927     * @param fragment The fragment to start
928     * @param push If true, the current fragment will be pushed onto the back stack.  If false,
929     * the current fragment will be replaced.
930     */
931    public void startPreferenceFragment(Fragment fragment, boolean push) {
932        FragmentTransaction transaction = getFragmentManager().openTransaction();
933        transaction.replace(com.android.internal.R.id.prefs, fragment);
934        if (push) {
935            transaction.addToBackStack(BACK_STACK_PREFS);
936        }
937        transaction.commit();
938    }
939
940    @Override
941    public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
942        Fragment f = Fragment.instantiate(this, pref.getFragment(), pref.getExtras());
943        startPreferenceFragment(f, true);
944        return true;
945    }
946
947    /**
948     * Posts a message to bind the preferences to the list view.
949     * <p>
950     * Binding late is preferred as any custom preference types created in
951     * {@link #onCreate(Bundle)} are able to have their views recycled.
952     */
953    private void postBindPreferences() {
954        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
955        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
956    }
957
958    private void bindPreferences() {
959        final PreferenceScreen preferenceScreen = getPreferenceScreen();
960        if (preferenceScreen != null) {
961            preferenceScreen.bind(getListView());
962            if (mSavedInstanceState != null) {
963                super.onRestoreInstanceState(mSavedInstanceState);
964                mSavedInstanceState = null;
965            }
966        }
967    }
968
969    /**
970     * Returns the {@link PreferenceManager} used by this activity.
971     * @return The {@link PreferenceManager}.
972     *
973     * @deprecated This function is not relevant for a modern fragment-based
974     * PreferenceActivity.
975     */
976    @Deprecated
977    public PreferenceManager getPreferenceManager() {
978        return mPreferenceManager;
979    }
980
981    private void requirePreferenceManager() {
982        if (mPreferenceManager == null) {
983            if (mAdapter == null) {
984                throw new RuntimeException("This should be called after super.onCreate.");
985            }
986            throw new RuntimeException(
987                    "Modern two-pane PreferenceActivity requires use of a PreferenceFragment");
988        }
989    }
990
991    /**
992     * Sets the root of the preference hierarchy that this activity is showing.
993     *
994     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
995     *
996     * @deprecated This function is not relevant for a modern fragment-based
997     * PreferenceActivity.
998     */
999    @Deprecated
1000    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
1001        requirePreferenceManager();
1002
1003        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
1004            postBindPreferences();
1005            CharSequence title = getPreferenceScreen().getTitle();
1006            // Set the title of the activity
1007            if (title != null) {
1008                setTitle(title);
1009            }
1010        }
1011    }
1012
1013    /**
1014     * Gets the root of the preference hierarchy that this activity is showing.
1015     *
1016     * @return The {@link PreferenceScreen} that is the root of the preference
1017     *         hierarchy.
1018     *
1019     * @deprecated This function is not relevant for a modern fragment-based
1020     * PreferenceActivity.
1021     */
1022    @Deprecated
1023    public PreferenceScreen getPreferenceScreen() {
1024        if (mPreferenceManager != null) {
1025            return mPreferenceManager.getPreferenceScreen();
1026        }
1027        return null;
1028    }
1029
1030    /**
1031     * Adds preferences from activities that match the given {@link Intent}.
1032     *
1033     * @param intent The {@link Intent} to query activities.
1034     *
1035     * @deprecated This function is not relevant for a modern fragment-based
1036     * PreferenceActivity.
1037     */
1038    @Deprecated
1039    public void addPreferencesFromIntent(Intent intent) {
1040        requirePreferenceManager();
1041
1042        setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
1043    }
1044
1045    /**
1046     * Inflates the given XML resource and adds the preference hierarchy to the current
1047     * preference hierarchy.
1048     *
1049     * @param preferencesResId The XML resource ID to inflate.
1050     *
1051     * @deprecated This function is not relevant for a modern fragment-based
1052     * PreferenceActivity.
1053     */
1054    @Deprecated
1055    public void addPreferencesFromResource(int preferencesResId) {
1056        requirePreferenceManager();
1057
1058        setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId,
1059                getPreferenceScreen()));
1060    }
1061
1062    /**
1063     * {@inheritDoc}
1064     *
1065     * @deprecated This function is not relevant for a modern fragment-based
1066     * PreferenceActivity.
1067     */
1068    @Deprecated
1069    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
1070        return false;
1071    }
1072
1073    /**
1074     * Finds a {@link Preference} based on its key.
1075     *
1076     * @param key The key of the preference to retrieve.
1077     * @return The {@link Preference} with the key, or null.
1078     * @see PreferenceGroup#findPreference(CharSequence)
1079     *
1080     * @deprecated This function is not relevant for a modern fragment-based
1081     * PreferenceActivity.
1082     */
1083    @Deprecated
1084    public Preference findPreference(CharSequence key) {
1085
1086        if (mPreferenceManager == null) {
1087            return null;
1088        }
1089
1090        return mPreferenceManager.findPreference(key);
1091    }
1092
1093    @Override
1094    protected void onNewIntent(Intent intent) {
1095        if (mPreferenceManager != null) {
1096            mPreferenceManager.dispatchNewIntent(intent);
1097        }
1098    }
1099
1100    // give subclasses access to the Next button
1101    /** @hide */
1102    protected boolean hasNextButton() {
1103        return mNextButton != null;
1104    }
1105    /** @hide */
1106    protected Button getNextButton() {
1107        return mNextButton;
1108    }
1109}
1110