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