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