PreferenceActivity.java revision b1ad5977bc8178b6d350ebe9099daded4c1ef603
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.ListActivity;
26import android.content.Context;
27import android.content.Intent;
28import android.content.res.Configuration;
29import android.content.res.TypedArray;
30import android.content.res.XmlResourceParser;
31import android.graphics.drawable.Drawable;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.text.TextUtils;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.util.Xml;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.View.OnClickListener;
43import android.widget.ArrayAdapter;
44import android.widget.Button;
45import android.widget.ImageView;
46import android.widget.ListView;
47import android.widget.TextView;
48
49import java.io.IOException;
50import java.util.ArrayList;
51import java.util.List;
52
53/**
54 * This is the base class for an activity to show a hierarchy of preferences
55 * to the user.  Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB}
56 * this class only allowed the display of a single set of preference; this
57 * functionality should now be found in the new {@link PreferenceFragment}
58 * class.  If you are using PreferenceActivity in its old mode, the documentation
59 * there applies to the deprecated APIs here.
60 *
61 * <p>This activity shows one or more headers of preferences, each of with
62 * is associated with a {@link PreferenceFragment} to display the preferences
63 * of that header.  The actual layout and display of these associations can
64 * however vary; currently there are two major approaches it may take:
65 *
66 * <ul>
67 * <li>On a small screen it may display only the headers as a single list
68 * when first launched.  Selecting one of the header items will re-launch
69 * the activity with it only showing the PreferenceFragment of that header.
70 * <li>On a large screen in may display both the headers and current
71 * PreferenceFragment together as panes.  Selecting a header item switches
72 * to showing the correct PreferenceFragment for that item.
73 * </ul>
74 *
75 * <p>Subclasses of PreferenceActivity should implement
76 * {@link #onBuildHeaders} to populate the header list with the desired
77 * items.  Doing this implicitly switches the class into its new "headers
78 * + fragments" mode rather than the old style of just showing a single
79 * preferences list.
80 *
81 * <a name="SampleCode"></a>
82 * <h3>Sample Code</h3>
83 *
84 * <p>The following sample code shows a simple preference activity that
85 * has two different sets of preferences.  The implementation, consisting
86 * of the activity itself as well as its two preference fragments is:</p>
87 *
88 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java
89 *      activity}
90 *
91 * <p>The preference_headers resource describes the headers to be displayed
92 * and the fragments associated with them.  It is:
93 *
94 * {@sample development/samples/ApiDemos/res/xml/preference_headers.xml headers}
95 *
96 * See {@link PreferenceFragment} for information on implementing the
97 * fragments themselves.
98 */
99public abstract class PreferenceActivity extends ListActivity implements
100        PreferenceManager.OnPreferenceTreeClickListener {
101    private static final String TAG = "PreferenceActivity";
102
103    private static final String PREFERENCES_TAG = "android:preferences";
104
105    private static final String EXTRA_PREFS_SHOW_FRAGMENT = ":android:show_fragment";
106
107    private static final String EXTRA_PREFS_NO_HEADERS = ":android:no_headers";
108
109    // extras that allow any preference activity to be launched as part of a wizard
110
111    // show Back and Next buttons? takes boolean parameter
112    // Back will then return RESULT_CANCELED and Next RESULT_OK
113    private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar";
114
115    // specify custom text for the Back or Next buttons, or cause a button to not appear
116    // at all by setting it to null
117    private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text";
118    private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text";
119
120    // --- State for new mode when showing a list of headers + prefs fragment
121
122    private final ArrayList<Header> mHeaders = new ArrayList<Header>();
123
124    private HeaderAdapter mAdapter;
125
126    private View mPrefsContainer;
127
128    private boolean mSinglePane;
129
130    // --- State for old mode when showing a single preference list
131
132    private PreferenceManager mPreferenceManager;
133
134    private Bundle mSavedInstanceState;
135
136    // --- Common state
137
138    private Button mNextButton;
139
140    /**
141     * The starting request code given out to preference framework.
142     */
143    private static final int FIRST_REQUEST_CODE = 100;
144
145    private static final int MSG_BIND_PREFERENCES = 0;
146    private Handler mHandler = new Handler() {
147        @Override
148        public void handleMessage(Message msg) {
149            switch (msg.what) {
150
151                case MSG_BIND_PREFERENCES:
152                    bindPreferences();
153                    break;
154            }
155        }
156    };
157
158    private class HeaderViewHolder {
159        ImageView icon;
160        TextView title;
161        TextView summary;
162    }
163
164    private class HeaderAdapter extends ArrayAdapter<Header> {
165        private LayoutInflater mInflater;
166
167        public HeaderAdapter(Context context, List<Header> objects) {
168            super(context, 0, objects);
169            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
170        }
171
172        @Override
173        public View getView(int position, View convertView, ViewGroup parent) {
174            HeaderViewHolder holder;
175            View view;
176
177            if (convertView == null) {
178                view = mInflater.inflate(com.android.internal.R.layout.preference_list_item,
179                        parent, false);
180                holder = new HeaderViewHolder();
181                holder.icon = (ImageView)view.findViewById(
182                        com.android.internal.R.id.icon);
183                holder.title = (TextView)view.findViewById(
184                        com.android.internal.R.id.title);
185                holder.summary = (TextView)view.findViewById(
186                        com.android.internal.R.id.summary);
187                view.setTag(holder);
188            } else {
189                view = convertView;
190                holder = (HeaderViewHolder)view.getTag();
191            }
192
193            Header header = getItem(position);
194            if (header.icon != null) holder.icon.setImageDrawable(header.icon);
195            else if (header.iconRes != 0) holder.icon.setImageResource(header.iconRes);
196            if (header.title != null) holder.title.setText(header.title);
197            if (header.summary != null) holder.summary.setText(header.summary);
198
199            return view;
200        }
201    }
202
203    /**
204     * Description of a single Header item that the user can select.
205     */
206    public static class Header {
207        /**
208         * Title of the header that is shown to the user.
209         */
210        CharSequence title;
211
212        /**
213         * Optional summary describing what this header controls.
214         */
215        CharSequence summary;
216
217        /**
218         * Optional icon resource to show for this header.
219         */
220        int iconRes;
221
222        /**
223         * Optional icon drawable to show for this header.  (If this is non-null,
224         * the iconRes will be ignored.)
225         */
226        Drawable icon;
227
228        /**
229         * Full class name of the fragment to display when this header is
230         * selected.
231         */
232        String fragment;
233    }
234
235    @Override
236    protected void onCreate(Bundle savedInstanceState) {
237        super.onCreate(savedInstanceState);
238
239        setContentView(com.android.internal.R.layout.preference_list_content);
240
241        mPrefsContainer = findViewById(com.android.internal.R.id.prefs);
242        boolean hidingHeaders = onIsHidingHeaders();
243        mSinglePane = hidingHeaders || !onIsMultiPane();
244        String initialFragment = getIntent().getStringExtra(EXTRA_PREFS_SHOW_FRAGMENT);
245
246        if (initialFragment != null && mSinglePane) {
247            // If we are just showing a fragment, we want to run in
248            // new fragment mode, but don't need to compute and show
249            // the headers.
250            getListView().setVisibility(View.GONE);
251            mPrefsContainer.setVisibility(View.VISIBLE);
252            switchToHeader(initialFragment);
253
254        } else {
255            // We need to try to build the headers.
256            onBuildHeaders(mHeaders);
257
258            // If there are headers, then at this point we need to show
259            // them and, depending on the screen, we may also show in-line
260            // the currently selected preference fragment.
261            if (mHeaders.size() > 0) {
262                mAdapter = new HeaderAdapter(this, mHeaders);
263                setListAdapter(mAdapter);
264                if (!mSinglePane) {
265                    mPrefsContainer.setVisibility(View.VISIBLE);
266                    switchToHeader(initialFragment != null
267                            ? initialFragment : onGetInitialFragment());
268                }
269
270            // If there are no headers, we are in the old "just show a screen
271            // of preferences" mode.
272            } else {
273                mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
274                mPreferenceManager.setOnPreferenceTreeClickListener(this);
275            }
276        }
277
278        getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
279
280        // see if we should show Back/Next buttons
281        Intent intent = getIntent();
282        if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
283
284            findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE);
285
286            Button backButton = (Button)findViewById(com.android.internal.R.id.back_button);
287            backButton.setOnClickListener(new OnClickListener() {
288                public void onClick(View v) {
289                    setResult(RESULT_CANCELED);
290                    finish();
291                }
292            });
293            mNextButton = (Button)findViewById(com.android.internal.R.id.next_button);
294            mNextButton.setOnClickListener(new OnClickListener() {
295                public void onClick(View v) {
296                    setResult(RESULT_OK);
297                    finish();
298                }
299            });
300
301            // set our various button parameters
302            if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) {
303                String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT);
304                if (TextUtils.isEmpty(buttonText)) {
305                    mNextButton.setVisibility(View.GONE);
306                }
307                else {
308                    mNextButton.setText(buttonText);
309                }
310            }
311            if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) {
312                String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT);
313                if (TextUtils.isEmpty(buttonText)) {
314                    backButton.setVisibility(View.GONE);
315                }
316                else {
317                    backButton.setText(buttonText);
318                }
319            }
320        }
321    }
322
323    /**
324     * Called to determine if the activity should run in multi-pane mode.
325     * The default implementation returns true if the screen is large
326     * enough.
327     */
328    public boolean onIsMultiPane() {
329        Configuration config = getResources().getConfiguration();
330        if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK)
331                == Configuration.SCREENLAYOUT_SIZE_XLARGE
332                && config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
333            return true;
334        }
335        return false;
336    }
337
338    /**
339     * Called to determine whether the header list should be hidden.  The
340     * default implementation hides the list if the activity is being re-launched
341     * when not in multi-pane mode.
342     */
343    public boolean onIsHidingHeaders() {
344        return getIntent().getBooleanExtra(EXTRA_PREFS_NO_HEADERS, false);
345    }
346
347    /**
348     * Called to determine the initial fragment to be shown.  The default
349     * implementation simply returns the fragment of the first header.
350     */
351    public String onGetInitialFragment() {
352        return mHeaders.get(0).fragment;
353    }
354
355    /**
356     * Called when the activity needs its list of headers build.  By
357     * implementing this and adding at least one item to the list, you
358     * will cause the activity to run in its modern fragment mode.  Note
359     * that this function may not always be called; for example, if the
360     * activity has been asked to display a particular fragment without
361     * the header list, there is no need to build the headers.
362     *
363     * <p>Typical implementations will use {@link #loadHeadersFromResource}
364     * to fill in the list from a resource.
365     *
366     * @param target The list in which to place the headers.
367     */
368    public void onBuildHeaders(List<Header> target) {
369    }
370
371    /**
372     * Parse the given XML file as a header description, adding each
373     * parsed Header into the target list.
374     *
375     * @param resid The XML resource to load and parse.
376     * @param target The list in which the parsed headers should be placed.
377     */
378    public void loadHeadersFromResource(int resid, List<Header> target) {
379        XmlResourceParser parser = null;
380        try {
381            parser = getResources().getXml(resid);
382            AttributeSet attrs = Xml.asAttributeSet(parser);
383
384            int type;
385            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
386                    && type != XmlPullParser.START_TAG) {
387            }
388
389            String nodeName = parser.getName();
390            if (!"PreferenceHeaders".equals(nodeName)) {
391                throw new RuntimeException(
392                        "XML document must start with <PreferenceHeaders> tag; found"
393                        + nodeName + " at " + parser.getPositionDescription());
394            }
395
396            int outerDepth = parser.getDepth();
397            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
398                   && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
399                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
400                    continue;
401                }
402
403                nodeName = parser.getName();
404                if ("Header".equals(nodeName)) {
405                    Header header = new Header();
406
407                    TypedArray sa = getResources().obtainAttributes(attrs,
408                            com.android.internal.R.styleable.PreferenceHeader);
409                    header.title = sa.getText(
410                            com.android.internal.R.styleable.PreferenceHeader_title);
411                    header.summary = sa.getText(
412                            com.android.internal.R.styleable.PreferenceHeader_summary);
413                    header.iconRes = sa.getResourceId(
414                            com.android.internal.R.styleable.PreferenceHeader_icon, 0);
415                    header.fragment = sa.getString(
416                            com.android.internal.R.styleable.PreferenceHeader_fragment);
417                    sa.recycle();
418
419                    target.add(header);
420
421                    XmlUtils.skipCurrentTag(parser);
422                } else {
423                    XmlUtils.skipCurrentTag(parser);
424                }
425            }
426
427        } catch (XmlPullParserException e) {
428            throw new RuntimeException("Error parsing headers", e);
429        } catch (IOException e) {
430            throw new RuntimeException("Error parsing headers", e);
431        } finally {
432            if (parser != null) parser.close();
433        }
434
435    }
436
437    @Override
438    protected void onStop() {
439        super.onStop();
440
441        if (mPreferenceManager != null) {
442            mPreferenceManager.dispatchActivityStop();
443        }
444    }
445
446    @Override
447    protected void onDestroy() {
448        super.onDestroy();
449
450        if (mPreferenceManager != null) {
451            mPreferenceManager.dispatchActivityDestroy();
452        }
453    }
454
455    @Override
456    protected void onSaveInstanceState(Bundle outState) {
457        super.onSaveInstanceState(outState);
458
459        if (mPreferenceManager != null) {
460            final PreferenceScreen preferenceScreen = getPreferenceScreen();
461            if (preferenceScreen != null) {
462                Bundle container = new Bundle();
463                preferenceScreen.saveHierarchyState(container);
464                outState.putBundle(PREFERENCES_TAG, container);
465            }
466        }
467    }
468
469    @Override
470    protected void onRestoreInstanceState(Bundle state) {
471        if (mPreferenceManager != null) {
472            Bundle container = state.getBundle(PREFERENCES_TAG);
473            if (container != null) {
474                final PreferenceScreen preferenceScreen = getPreferenceScreen();
475                if (preferenceScreen != null) {
476                    preferenceScreen.restoreHierarchyState(container);
477                    mSavedInstanceState = state;
478                    return;
479                }
480            }
481        }
482
483        // Only call this if we didn't save the instance state for later.
484        // If we did save it, it will be restored when we bind the adapter.
485        super.onRestoreInstanceState(state);
486    }
487
488    @Override
489    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
490        super.onActivityResult(requestCode, resultCode, data);
491
492        if (mPreferenceManager != null) {
493            mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
494        }
495    }
496
497    @Override
498    public void onContentChanged() {
499        super.onContentChanged();
500
501        if (mPreferenceManager != null) {
502            postBindPreferences();
503        }
504    }
505
506    @Override
507    protected void onListItemClick(ListView l, View v, int position, long id) {
508        super.onListItemClick(l, v, position, id);
509
510        if (mAdapter != null) {
511            onHeaderClick(mHeaders.get(position), position);
512        }
513    }
514
515    /**
516     * Called when the user selects an item in the header list.  The default
517     * implementation will call either {@link #startWithFragment(String)}
518     * or {@link #switchToHeader(String)} as appropriate.
519     *
520     * @param header The header that was selected.
521     * @param position The header's position in the list.
522     */
523    public void onHeaderClick(Header header, int position) {
524        if (mSinglePane) {
525            startWithFragment(header.fragment);
526        } else {
527            switchToHeader(header.fragment);
528        }
529    }
530
531    /**
532     * Start a new instance of this activity, showing only the given
533     * preference fragment.  When launched in this mode, the header list
534     * will be hidden and the given preference fragment will be instantiated
535     * and fill the entire activity.
536     *
537     * @param fragmentName The name of the fragment to display.
538     */
539    public void startWithFragment(String fragmentName) {
540        Intent intent = new Intent(Intent.ACTION_MAIN);
541        intent.setClass(this, getClass());
542        intent.putExtra(EXTRA_PREFS_SHOW_FRAGMENT, fragmentName);
543        intent.putExtra(EXTRA_PREFS_NO_HEADERS, true);
544        startActivity(intent);
545    }
546
547    /**
548     * When in two-pane mode, switch the fragment pane to show the given
549     * preference fragment.
550     *
551     * @param fragmentName The name of the fragment to display.
552     */
553    public void switchToHeader(String fragmentName) {
554        Fragment f;
555        try {
556            f = Fragment.instantiate(this, fragmentName);
557        } catch (Exception e) {
558            Log.w(TAG, "Failure instantiating fragment " + fragmentName, e);
559            return;
560        }
561        openFragmentTransaction().replace(com.android.internal.R.id.prefs, f).commit();
562    }
563
564    /**
565     * Posts a message to bind the preferences to the list view.
566     * <p>
567     * Binding late is preferred as any custom preference types created in
568     * {@link #onCreate(Bundle)} are able to have their views recycled.
569     */
570    private void postBindPreferences() {
571        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
572        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
573    }
574
575    private void bindPreferences() {
576        final PreferenceScreen preferenceScreen = getPreferenceScreen();
577        if (preferenceScreen != null) {
578            preferenceScreen.bind(getListView());
579            if (mSavedInstanceState != null) {
580                super.onRestoreInstanceState(mSavedInstanceState);
581                mSavedInstanceState = null;
582            }
583        }
584    }
585
586    /**
587     * Returns the {@link PreferenceManager} used by this activity.
588     * @return The {@link PreferenceManager}.
589     *
590     * @deprecated This function is not relevant for a modern fragment-based
591     * PreferenceActivity.
592     */
593    @Deprecated
594    public PreferenceManager getPreferenceManager() {
595        return mPreferenceManager;
596    }
597
598    private void requirePreferenceManager() {
599        if (mPreferenceManager == null) {
600            if (mAdapter == null) {
601                throw new RuntimeException("This should be called after super.onCreate.");
602            }
603            throw new RuntimeException(
604                    "Modern two-pane PreferenceActivity requires use of a PreferenceFragment");
605        }
606    }
607
608    /**
609     * Sets the root of the preference hierarchy that this activity is showing.
610     *
611     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
612     *
613     * @deprecated This function is not relevant for a modern fragment-based
614     * PreferenceActivity.
615     */
616    @Deprecated
617    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
618        requirePreferenceManager();
619
620        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
621            postBindPreferences();
622            CharSequence title = getPreferenceScreen().getTitle();
623            // Set the title of the activity
624            if (title != null) {
625                setTitle(title);
626            }
627        }
628    }
629
630    /**
631     * Gets the root of the preference hierarchy that this activity is showing.
632     *
633     * @return The {@link PreferenceScreen} that is the root of the preference
634     *         hierarchy.
635     *
636     * @deprecated This function is not relevant for a modern fragment-based
637     * PreferenceActivity.
638     */
639    @Deprecated
640    public PreferenceScreen getPreferenceScreen() {
641        if (mPreferenceManager != null) {
642            return mPreferenceManager.getPreferenceScreen();
643        }
644        return null;
645    }
646
647    /**
648     * Adds preferences from activities that match the given {@link Intent}.
649     *
650     * @param intent The {@link Intent} to query activities.
651     *
652     * @deprecated This function is not relevant for a modern fragment-based
653     * PreferenceActivity.
654     */
655    @Deprecated
656    public void addPreferencesFromIntent(Intent intent) {
657        requirePreferenceManager();
658
659        setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
660    }
661
662    /**
663     * Inflates the given XML resource and adds the preference hierarchy to the current
664     * preference hierarchy.
665     *
666     * @param preferencesResId The XML resource ID to inflate.
667     *
668     * @deprecated This function is not relevant for a modern fragment-based
669     * PreferenceActivity.
670     */
671    @Deprecated
672    public void addPreferencesFromResource(int preferencesResId) {
673        requirePreferenceManager();
674
675        setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId,
676                getPreferenceScreen()));
677    }
678
679    /**
680     * {@inheritDoc}
681     *
682     * @deprecated This function is not relevant for a modern fragment-based
683     * PreferenceActivity.
684     */
685    @Deprecated
686    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
687        return false;
688    }
689
690    /**
691     * Finds a {@link Preference} based on its key.
692     *
693     * @param key The key of the preference to retrieve.
694     * @return The {@link Preference} with the key, or null.
695     * @see PreferenceGroup#findPreference(CharSequence)
696     *
697     * @deprecated This function is not relevant for a modern fragment-based
698     * PreferenceActivity.
699     */
700    @Deprecated
701    public Preference findPreference(CharSequence key) {
702
703        if (mPreferenceManager == null) {
704            return null;
705        }
706
707        return mPreferenceManager.findPreference(key);
708    }
709
710    @Override
711    protected void onNewIntent(Intent intent) {
712        if (mPreferenceManager != null) {
713            mPreferenceManager.dispatchNewIntent(intent);
714        }
715    }
716
717    // give subclasses access to the Next button
718    /** @hide */
719    protected boolean hasNextButton() {
720        return mNextButton != null;
721    }
722    /** @hide */
723    protected Button getNextButton() {
724        return mNextButton;
725    }
726}
727