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