PreferenceFragment.java revision 5e18c40d28cba98607d34f50e972c4d977c3dd6c
1/*
2 * Copyright (C) 2010 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 android.app.Activity;
20import android.app.Fragment;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.content.res.TypedArray;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.Message;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.View.OnKeyListener;
32import android.widget.ListView;
33
34/**
35 * Shows a hierarchy of {@link Preference} objects as
36 * lists. These preferences will
37 * automatically save to {@link SharedPreferences} as the user interacts with
38 * them. To retrieve an instance of {@link SharedPreferences} that the
39 * preference hierarchy in this fragment will use, call
40 * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)}
41 * with a context in the same package as this fragment.
42 * <p>
43 * Furthermore, the preferences shown will follow the visual style of system
44 * preferences. It is easy to create a hierarchy of preferences (that can be
45 * shown on multiple screens) via XML. For these reasons, it is recommended to
46 * use this fragment (as a superclass) to deal with preferences in applications.
47 * <p>
48 * A {@link PreferenceScreen} object should be at the top of the preference
49 * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy
50 * denote a screen break--that is the preferences contained within subsequent
51 * {@link PreferenceScreen} should be shown on another screen. The preference
52 * framework handles showing these other screens from the preference hierarchy.
53 * <p>
54 * The preference hierarchy can be formed in multiple ways:
55 * <li> From an XML file specifying the hierarchy
56 * <li> From different {@link Activity Activities} that each specify its own
57 * preferences in an XML file via {@link Activity} meta-data
58 * <li> From an object hierarchy rooted with {@link PreferenceScreen}
59 * <p>
60 * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The
61 * root element should be a {@link PreferenceScreen}. Subsequent elements can point
62 * to actual {@link Preference} subclasses. As mentioned above, subsequent
63 * {@link PreferenceScreen} in the hierarchy will result in the screen break.
64 * <p>
65 * To specify an {@link Intent} to query {@link Activity Activities} that each
66 * have preferences, use {@link #addPreferencesFromIntent}. Each
67 * {@link Activity} can specify meta-data in the manifest (via the key
68 * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML
69 * resource. These XML resources will be inflated into a single preference
70 * hierarchy and shown by this fragment.
71 * <p>
72 * To specify an object hierarchy rooted with {@link PreferenceScreen}, use
73 * {@link #setPreferenceScreen(PreferenceScreen)}.
74 * <p>
75 * As a convenience, this fragment implements a click listener for any
76 * preference in the current hierarchy, see
77 * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}.
78 *
79 * <div class="special reference">
80 * <h3>Developer Guides</h3>
81 * <p>For information about using {@code PreferenceFragment},
82 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
83 * guide.</p>
84 * </div>
85 *
86 * <a name="SampleCode"></a>
87 * <h3>Sample Code</h3>
88 *
89 * <p>The following sample code shows a simple preference fragment that is
90 * populated from a resource.  The resource it loads is:</p>
91 *
92 * {@sample development/samples/ApiDemos/res/xml/preferences.xml preferences}
93 *
94 * <p>The fragment implementation itself simply populates the preferences
95 * when created.  Note that the preferences framework takes care of loading
96 * the current values out of the app preferences and writing them when changed:</p>
97 *
98 * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/FragmentPreferences.java
99 *      fragment}
100 *
101 * @see Preference
102 * @see PreferenceScreen
103 */
104public abstract class PreferenceFragment extends Fragment implements
105        PreferenceManager.OnPreferenceTreeClickListener {
106
107    private static final String PREFERENCES_TAG = "android:preferences";
108
109    private PreferenceManager mPreferenceManager;
110    private ListView mList;
111    private boolean mHavePrefs;
112    private boolean mInitDone;
113
114    private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment;
115
116    /**
117     * The starting request code given out to preference framework.
118     */
119    private static final int FIRST_REQUEST_CODE = 100;
120
121    private static final int MSG_BIND_PREFERENCES = 1;
122    private Handler mHandler = new Handler() {
123        @Override
124        public void handleMessage(Message msg) {
125            switch (msg.what) {
126
127                case MSG_BIND_PREFERENCES:
128                    bindPreferences();
129                    break;
130            }
131        }
132    };
133
134    final private Runnable mRequestFocus = new Runnable() {
135        public void run() {
136            mList.focusableViewAvailable(mList);
137        }
138    };
139
140    /**
141     * Interface that PreferenceFragment's containing activity should
142     * implement to be able to process preference items that wish to
143     * switch to a new fragment.
144     */
145    public interface OnPreferenceStartFragmentCallback {
146        /**
147         * Called when the user has clicked on a Preference that has
148         * a fragment class name associated with it.  The implementation
149         * to should instantiate and switch to an instance of the given
150         * fragment.
151         */
152        boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
153    }
154
155    @Override
156    public void onCreate(Bundle savedInstanceState) {
157        super.onCreate(savedInstanceState);
158        mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE);
159        mPreferenceManager.setFragment(this);
160    }
161
162    @Override
163    public View onCreateView(LayoutInflater inflater, ViewGroup container,
164            Bundle savedInstanceState) {
165
166        TypedArray a = getActivity().obtainStyledAttributes(null,
167                com.android.internal.R.styleable.PreferenceFragment,
168                com.android.internal.R.attr.preferenceFragmentStyle,
169                0);
170
171        mLayoutResId = a.getResourceId(com.android.internal.R.styleable.PreferenceFragment_layout,
172                mLayoutResId);
173
174        a.recycle();
175
176        return inflater.inflate(mLayoutResId, container, false);
177    }
178
179    @Override
180    public void onActivityCreated(Bundle savedInstanceState) {
181        super.onActivityCreated(savedInstanceState);
182
183        if (mHavePrefs) {
184            bindPreferences();
185        }
186
187        mInitDone = true;
188
189        if (savedInstanceState != null) {
190            Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
191            if (container != null) {
192                final PreferenceScreen preferenceScreen = getPreferenceScreen();
193                if (preferenceScreen != null) {
194                    preferenceScreen.restoreHierarchyState(container);
195                }
196            }
197        }
198    }
199
200    @Override
201    public void onStart() {
202        super.onStart();
203        mPreferenceManager.setOnPreferenceTreeClickListener(this);
204    }
205
206    @Override
207    public void onStop() {
208        super.onStop();
209        mPreferenceManager.dispatchActivityStop();
210        mPreferenceManager.setOnPreferenceTreeClickListener(null);
211    }
212
213    @Override
214    public void onDestroyView() {
215        if (mList != null) {
216            mList.setOnKeyListener(null);
217        }
218        mList = null;
219        mHandler.removeCallbacks(mRequestFocus);
220        mHandler.removeMessages(MSG_BIND_PREFERENCES);
221        super.onDestroyView();
222    }
223
224    @Override
225    public void onDestroy() {
226        super.onDestroy();
227        mPreferenceManager.dispatchActivityDestroy();
228    }
229
230    @Override
231    public void onSaveInstanceState(Bundle outState) {
232        super.onSaveInstanceState(outState);
233
234        final PreferenceScreen preferenceScreen = getPreferenceScreen();
235        if (preferenceScreen != null) {
236            Bundle container = new Bundle();
237            preferenceScreen.saveHierarchyState(container);
238            outState.putBundle(PREFERENCES_TAG, container);
239        }
240    }
241
242    @Override
243    public void onActivityResult(int requestCode, int resultCode, Intent data) {
244        super.onActivityResult(requestCode, resultCode, data);
245
246        mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
247    }
248
249    /**
250     * Returns the {@link PreferenceManager} used by this fragment.
251     * @return The {@link PreferenceManager}.
252     */
253    public PreferenceManager getPreferenceManager() {
254        return mPreferenceManager;
255    }
256
257    /**
258     * Sets the root of the preference hierarchy that this fragment is showing.
259     *
260     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
261     */
262    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
263        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
264            onUnbindPreferences();
265            mHavePrefs = true;
266            if (mInitDone) {
267                postBindPreferences();
268            }
269        }
270    }
271
272    /**
273     * Gets the root of the preference hierarchy that this fragment is showing.
274     *
275     * @return The {@link PreferenceScreen} that is the root of the preference
276     *         hierarchy.
277     */
278    public PreferenceScreen getPreferenceScreen() {
279        return mPreferenceManager.getPreferenceScreen();
280    }
281
282    /**
283     * Adds preferences from activities that match the given {@link Intent}.
284     *
285     * @param intent The {@link Intent} to query activities.
286     */
287    public void addPreferencesFromIntent(Intent intent) {
288        requirePreferenceManager();
289
290        setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
291    }
292
293    /**
294     * Inflates the given XML resource and adds the preference hierarchy to the current
295     * preference hierarchy.
296     *
297     * @param preferencesResId The XML resource ID to inflate.
298     */
299    public void addPreferencesFromResource(int preferencesResId) {
300        requirePreferenceManager();
301
302        setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(),
303                preferencesResId, getPreferenceScreen()));
304    }
305
306    /**
307     * {@inheritDoc}
308     */
309    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
310            Preference preference) {
311        if (preference.getFragment() != null &&
312                getActivity() instanceof OnPreferenceStartFragmentCallback) {
313            return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
314                    this, preference);
315        }
316        return false;
317    }
318
319    /**
320     * Finds a {@link Preference} based on its key.
321     *
322     * @param key The key of the preference to retrieve.
323     * @return The {@link Preference} with the key, or null.
324     * @see PreferenceGroup#findPreference(CharSequence)
325     */
326    public Preference findPreference(CharSequence key) {
327        if (mPreferenceManager == null) {
328            return null;
329        }
330        return mPreferenceManager.findPreference(key);
331    }
332
333    private void requirePreferenceManager() {
334        if (mPreferenceManager == null) {
335            throw new RuntimeException("This should be called after super.onCreate.");
336        }
337    }
338
339    private void postBindPreferences() {
340        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
341        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
342    }
343
344    private void bindPreferences() {
345        final PreferenceScreen preferenceScreen = getPreferenceScreen();
346        if (preferenceScreen != null) {
347            preferenceScreen.bind(getListView());
348        }
349        onBindPreferences();
350    }
351
352    /** @hide */
353    protected void onBindPreferences() {
354    }
355
356    /** @hide */
357    protected void onUnbindPreferences() {
358    }
359
360    /** @hide */
361    public ListView getListView() {
362        ensureList();
363        return mList;
364    }
365
366    /** @hide */
367    public boolean hasListView() {
368        if (mList != null) {
369            return true;
370        }
371        View root = getView();
372        if (root == null) {
373            return false;
374        }
375        View rawListView = root.findViewById(android.R.id.list);
376        if (!(rawListView instanceof ListView)) {
377            return false;
378        }
379        mList = (ListView)rawListView;
380        if (mList == null) {
381            return false;
382        }
383        return true;
384    }
385
386    private void ensureList() {
387        if (mList != null) {
388            return;
389        }
390        View root = getView();
391        if (root == null) {
392            throw new IllegalStateException("Content view not yet created");
393        }
394        View rawListView = root.findViewById(android.R.id.list);
395        if (!(rawListView instanceof ListView)) {
396            throw new RuntimeException(
397                    "Content has view with id attribute 'android.R.id.list' "
398                    + "that is not a ListView class");
399        }
400        mList = (ListView)rawListView;
401        if (mList == null) {
402            throw new RuntimeException(
403                    "Your content must have a ListView whose id attribute is " +
404                    "'android.R.id.list'");
405        }
406        mList.setOnKeyListener(mListOnKeyListener);
407        mHandler.post(mRequestFocus);
408    }
409
410    private OnKeyListener mListOnKeyListener = new OnKeyListener() {
411
412        @Override
413        public boolean onKey(View v, int keyCode, KeyEvent event) {
414            Object selectedItem = mList.getSelectedItem();
415            if (selectedItem instanceof Preference) {
416                View selectedView = mList.getSelectedView();
417                return ((Preference)selectedItem).onKey(
418                        selectedView, keyCode, event);
419            }
420            return false;
421        }
422
423    };
424}
425