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