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