PreferenceManager.java revision c39d9c75590eca86a5e7e32a8824ba04a0d42e9b
1/*
2 * Copyright (C) 2015 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.support.v7.preference;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.support.annotation.RestrictTo;
22import android.support.v4.content.ContextCompat;
23import android.support.v4.content.SharedPreferencesCompat;
24import android.support.v4.os.BuildCompat;
25
26import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
27
28/**
29 * Used to help create {@link Preference} hierarchies
30 * from activities or XML.
31 * <p>
32 * In most cases, clients should use
33 * {@link android.support.v14.preference.PreferenceFragment#addPreferencesFromResource(int)}, or
34 * {@link PreferenceFragmentCompat#addPreferencesFromResource(int)}.
35 *
36 * @see android.support.v14.preference.PreferenceFragment
37 * @see PreferenceFragmentCompat
38 */
39public class PreferenceManager {
40
41    private static final String TAG = "PreferenceManager";
42
43    public static final String KEY_HAS_SET_DEFAULT_VALUES = "_has_set_default_values";
44
45    /**
46     * The context to use. This should always be set.
47     */
48    private Context mContext;
49
50    /**
51     * The counter for unique IDs.
52     */
53    private long mNextId = 0;
54
55    /**
56     * Cached shared preferences.
57     */
58    private SharedPreferences mSharedPreferences;
59
60    /**
61     * If in no-commit mode, the shared editor to give out (which will be
62     * committed when exiting no-commit mode).
63     */
64    private SharedPreferences.Editor mEditor;
65
66    /**
67     * Blocks commits from happening on the shared editor. This is used when
68     * inflating the hierarchy. Do not set this directly, use {@link #setNoCommit(boolean)}
69     */
70    private boolean mNoCommit;
71
72    /**
73     * The SharedPreferences name that will be used for all {@link Preference}s
74     * managed by this instance.
75     */
76    private String mSharedPreferencesName;
77
78    /**
79     * The SharedPreferences mode that will be used for all {@link Preference}s
80     * managed by this instance.
81     */
82    private int mSharedPreferencesMode;
83
84    private static final int STORAGE_DEFAULT = 0;
85    private static final int STORAGE_DEVICE_PROTECTED = 1;
86
87    private int mStorage = STORAGE_DEFAULT;
88
89    /**
90     * The {@link PreferenceScreen} at the root of the preference hierarchy.
91     */
92    private PreferenceScreen mPreferenceScreen;
93
94    private OnPreferenceTreeClickListener mOnPreferenceTreeClickListener;
95    private OnDisplayPreferenceDialogListener mOnDisplayPreferenceDialogListener;
96    private OnNavigateToScreenListener mOnNavigateToScreenListener;
97
98    /**
99     * @hide
100     */
101    @RestrictTo(GROUP_ID)
102    public PreferenceManager(Context context) {
103        mContext = context;
104
105        setSharedPreferencesName(getDefaultSharedPreferencesName(context));
106    }
107
108    /**
109     * Inflates a preference hierarchy from XML. If a preference hierarchy is
110     * given, the new preference hierarchies will be merged in.
111     *
112     * @param context The context of the resource.
113     * @param resId The resource ID of the XML to inflate.
114     * @param rootPreferences Optional existing hierarchy to merge the new
115     *            hierarchies into.
116     * @return The root hierarchy (if one was not provided, the new hierarchy's
117     *         root).
118     * @hide
119     */
120    @RestrictTo(GROUP_ID)
121    public PreferenceScreen inflateFromResource(Context context, int resId,
122            PreferenceScreen rootPreferences) {
123        // Block commits
124        setNoCommit(true);
125
126        final PreferenceInflater inflater = new PreferenceInflater(context, this);
127        rootPreferences = (PreferenceScreen) inflater.inflate(resId, rootPreferences);
128        rootPreferences.onAttachedToHierarchy(this);
129
130        // Unblock commits
131        setNoCommit(false);
132
133        return rootPreferences;
134    }
135
136    public PreferenceScreen createPreferenceScreen(Context context) {
137        final PreferenceScreen preferenceScreen = new PreferenceScreen(context, null);
138        preferenceScreen.onAttachedToHierarchy(this);
139        return preferenceScreen;
140    }
141
142    /**
143     * Called by a preference to get a unique ID in its hierarchy.
144     *
145     * @return A unique ID.
146     */
147    long getNextId() {
148        synchronized (this) {
149            return mNextId++;
150        }
151    }
152
153    /**
154     * Returns the current name of the SharedPreferences file that preferences managed by
155     * this will use.
156     *
157     * @return The name that can be passed to {@link Context#getSharedPreferences(String, int)}.
158     * @see Context#getSharedPreferences(String, int)
159     */
160    public String getSharedPreferencesName() {
161        return mSharedPreferencesName;
162    }
163
164    /**
165     * Sets the name of the SharedPreferences file that preferences managed by this
166     * will use.
167     *
168     * @param sharedPreferencesName The name of the SharedPreferences file.
169     * @see Context#getSharedPreferences(String, int)
170     */
171    public void setSharedPreferencesName(String sharedPreferencesName) {
172        mSharedPreferencesName = sharedPreferencesName;
173        mSharedPreferences = null;
174    }
175
176    /**
177     * Returns the current mode of the SharedPreferences file that preferences managed by
178     * this will use.
179     *
180     * @return The mode that can be passed to {@link Context#getSharedPreferences(String, int)}.
181     * @see Context#getSharedPreferences(String, int)
182     */
183    public int getSharedPreferencesMode() {
184        return mSharedPreferencesMode;
185    }
186
187    /**
188     * Sets the mode of the SharedPreferences file that preferences managed by this
189     * will use.
190     *
191     * @param sharedPreferencesMode The mode of the SharedPreferences file.
192     * @see Context#getSharedPreferences(String, int)
193     */
194    public void setSharedPreferencesMode(int sharedPreferencesMode) {
195        mSharedPreferencesMode = sharedPreferencesMode;
196        mSharedPreferences = null;
197    }
198
199    /**
200     * Sets the storage location used internally by this class to be the default
201     * provided by the hosting {@link Context}.
202     */
203    public void setStorageDefault() {
204        if (BuildCompat.isAtLeastN()) {
205            mStorage = STORAGE_DEFAULT;
206            mSharedPreferences = null;
207        }
208    }
209
210    /**
211     * Explicitly set the storage location used internally by this class to be
212     * device-protected storage.
213     * <p>
214     * On devices with direct boot, data stored in this location is encrypted
215     * with a key tied to the physical device, and it can be accessed
216     * immediately after the device has booted successfully, both
217     * <em>before and after</em> the user has authenticated with their
218     * credentials (such as a lock pattern or PIN).
219     * <p>
220     * Because device-protected data is available without user authentication,
221     * you should carefully limit the data you store using this Context. For
222     * example, storing sensitive authentication tokens or passwords in the
223     * device-protected area is strongly discouraged.
224     * <p>
225     * Prior to {@link BuildCompat#isAtLeastN()} this method has no effect,
226     * since device-protected storage is not available.
227     *
228     * @see Context#createDeviceProtectedStorageContext()
229     */
230    public void setStorageDeviceProtected() {
231        if (BuildCompat.isAtLeastN()) {
232            mStorage = STORAGE_DEVICE_PROTECTED;
233            mSharedPreferences = null;
234        }
235    }
236
237    /**
238     * @removed
239     * @deprecated
240     */
241    @Deprecated
242    public void setStorageDeviceEncrypted() {
243        setStorageDeviceProtected();
244    }
245
246    /**
247     * Indicates if the storage location used internally by this class is the
248     * default provided by the hosting {@link Context}.
249     *
250     * @see #setStorageDefault()
251     * @see #setStorageDeviceProtected()
252     */
253    public boolean isStorageDefault() {
254        if (BuildCompat.isAtLeastN()) {
255            return mStorage == STORAGE_DEFAULT;
256        } else {
257            return true;
258        }
259    }
260
261    /**
262     * Indicates if the storage location used internally by this class is backed
263     * by device-protected storage.
264     *
265     * @see #setStorageDefault()
266     * @see #setStorageDeviceProtected()
267     */
268    public boolean isStorageDeviceProtected() {
269        if (BuildCompat.isAtLeastN()) {
270            return mStorage == STORAGE_DEVICE_PROTECTED;
271        } else {
272            return false;
273        }
274    }
275
276    /**
277     * Gets a SharedPreferences instance that preferences managed by this will
278     * use.
279     *
280     * @return A SharedPreferences instance pointing to the file that contains
281     *         the values of preferences that are managed by this.
282     */
283    public SharedPreferences getSharedPreferences() {
284        if (mSharedPreferences == null) {
285            final Context storageContext;
286            switch (mStorage) {
287                case STORAGE_DEVICE_PROTECTED:
288                    storageContext = ContextCompat.createDeviceProtectedStorageContext(mContext);
289                    break;
290                default:
291                    storageContext = mContext;
292                    break;
293            }
294
295            mSharedPreferences = storageContext.getSharedPreferences(mSharedPreferencesName,
296                    mSharedPreferencesMode);
297        }
298
299        return mSharedPreferences;
300    }
301
302    /**
303     * Gets a SharedPreferences instance that points to the default file that is
304     * used by the preference framework in the given context.
305     *
306     * @param context The context of the preferences whose values are wanted.
307     * @return A SharedPreferences instance that can be used to retrieve and
308     *         listen to values of the preferences.
309     */
310    public static SharedPreferences getDefaultSharedPreferences(Context context) {
311        return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
312                getDefaultSharedPreferencesMode());
313    }
314
315    private static String getDefaultSharedPreferencesName(Context context) {
316        return context.getPackageName() + "_preferences";
317    }
318
319    private static int getDefaultSharedPreferencesMode() {
320        return Context.MODE_PRIVATE;
321    }
322
323    /**
324     * Returns the root of the preference hierarchy managed by this class.
325     *
326     * @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
327     */
328    public PreferenceScreen getPreferenceScreen() {
329        return mPreferenceScreen;
330    }
331
332    /**
333     * Sets the root of the preference hierarchy.
334     *
335     * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
336     * @return Whether the {@link PreferenceScreen} given is different than the previous.
337     */
338    public boolean setPreferences(PreferenceScreen preferenceScreen) {
339        if (preferenceScreen != mPreferenceScreen) {
340            if (mPreferenceScreen != null) {
341                mPreferenceScreen.onDetached();
342            }
343            mPreferenceScreen = preferenceScreen;
344            return true;
345        }
346
347        return false;
348    }
349
350    /**
351     * Finds a {@link Preference} based on its key.
352     *
353     * @param key The key of the preference to retrieve.
354     * @return The {@link Preference} with the key, or null.
355     * @see PreferenceGroup#findPreference(CharSequence)
356     */
357    public Preference findPreference(CharSequence key) {
358        if (mPreferenceScreen == null) {
359            return null;
360        }
361
362        return mPreferenceScreen.findPreference(key);
363    }
364
365    /**
366     * Sets the default values from an XML preference file by reading the values defined
367     * by each {@link Preference} item's {@code android:defaultValue} attribute. This should
368     * be called by the application's main activity.
369     * <p>
370     *
371     * @param context The context of the shared preferences.
372     * @param resId The resource ID of the preference XML file.
373     * @param readAgain Whether to re-read the default values.
374     * If false, this method sets the default values only if this
375     * method has never been called in the past (or if the
376     * {@link #KEY_HAS_SET_DEFAULT_VALUES} in the default value shared
377     * preferences file is false). To attempt to set the default values again
378     * bypassing this check, set {@code readAgain} to true.
379     *            <p class="note">
380     *            Note: this will NOT reset preferences back to their default
381     *            values. For that functionality, use
382     *            {@link PreferenceManager#getDefaultSharedPreferences(Context)}
383     *            and clear it followed by a call to this method with this
384     *            parameter set to true.
385     */
386    public static void setDefaultValues(Context context, int resId, boolean readAgain) {
387
388        // Use the default shared preferences name and mode
389        setDefaultValues(context, getDefaultSharedPreferencesName(context),
390                getDefaultSharedPreferencesMode(), resId, readAgain);
391    }
392
393    /**
394     * Similar to {@link #setDefaultValues(Context, int, boolean)} but allows
395     * the client to provide the filename and mode of the shared preferences
396     * file.
397     *
398     * @param context The context of the shared preferences.
399     * @param sharedPreferencesName A custom name for the shared preferences file.
400     * @param sharedPreferencesMode The file creation mode for the shared preferences file, such
401     * as {@link android.content.Context#MODE_PRIVATE} or {@link
402     * android.content.Context#MODE_PRIVATE}
403     * @param resId The resource ID of the preference XML file.
404     * @param readAgain Whether to re-read the default values.
405     * If false, this method will set the default values only if this
406     * method has never been called in the past (or if the
407     * {@link #KEY_HAS_SET_DEFAULT_VALUES} in the default value shared
408     * preferences file is false). To attempt to set the default values again
409     * bypassing this check, set {@code readAgain} to true.
410     *            <p class="note">
411     *            Note: this will NOT reset preferences back to their default
412     *            values. For that functionality, use
413     *            {@link PreferenceManager#getDefaultSharedPreferences(Context)}
414     *            and clear it followed by a call to this method with this
415     *            parameter set to true.
416     *
417     * @see #setDefaultValues(Context, int, boolean)
418     * @see #setSharedPreferencesName(String)
419     * @see #setSharedPreferencesMode(int)
420     */
421    public static void setDefaultValues(Context context, String sharedPreferencesName,
422            int sharedPreferencesMode, int resId, boolean readAgain) {
423        final SharedPreferences defaultValueSp = context.getSharedPreferences(
424                KEY_HAS_SET_DEFAULT_VALUES, Context.MODE_PRIVATE);
425
426        if (readAgain || !defaultValueSp.getBoolean(KEY_HAS_SET_DEFAULT_VALUES, false)) {
427            final PreferenceManager pm = new PreferenceManager(context);
428            pm.setSharedPreferencesName(sharedPreferencesName);
429            pm.setSharedPreferencesMode(sharedPreferencesMode);
430            pm.inflateFromResource(context, resId, null);
431
432            SharedPreferences.Editor editor =
433                    defaultValueSp.edit().putBoolean(KEY_HAS_SET_DEFAULT_VALUES, true);
434
435            SharedPreferencesCompat.EditorCompat.getInstance().apply(editor);
436        }
437    }
438
439    /**
440     * Returns an editor to use when modifying the shared preferences.
441     * <p>
442     * Do NOT commit unless {@link #shouldCommit()} returns true.
443     *
444     * @return An editor to use to write to shared preferences.
445     * @see #shouldCommit()
446     */
447    SharedPreferences.Editor getEditor() {
448
449        if (mNoCommit) {
450            if (mEditor == null) {
451                mEditor = getSharedPreferences().edit();
452            }
453
454            return mEditor;
455        } else {
456            return getSharedPreferences().edit();
457        }
458    }
459
460    /**
461     * Whether it is the client's responsibility to commit on the
462     * {@link #getEditor()}. This will return false in cases where the writes
463     * should be batched, for example when inflating preferences from XML.
464     *
465     * @return Whether the client should commit.
466     */
467    boolean shouldCommit() {
468        return !mNoCommit;
469    }
470
471    private void setNoCommit(boolean noCommit) {
472        if (!noCommit && mEditor != null) {
473            SharedPreferencesCompat.EditorCompat.getInstance().apply(mEditor);
474        }
475        mNoCommit = noCommit;
476    }
477
478    /**
479     * Returns the context.
480     *
481     * @return The context.
482     */
483    public Context getContext() {
484        return mContext;
485    }
486
487    public OnDisplayPreferenceDialogListener getOnDisplayPreferenceDialogListener() {
488        return mOnDisplayPreferenceDialogListener;
489    }
490
491    public void setOnDisplayPreferenceDialogListener(
492            OnDisplayPreferenceDialogListener onDisplayPreferenceDialogListener) {
493        mOnDisplayPreferenceDialogListener = onDisplayPreferenceDialogListener;
494    }
495
496    /**
497     * Called when a preference requests that a dialog be shown to complete a user interaction.
498     *
499     * @param preference The preference requesting the dialog.
500     */
501    public void showDialog(Preference preference) {
502        if (mOnDisplayPreferenceDialogListener != null) {
503            mOnDisplayPreferenceDialogListener.onDisplayPreferenceDialog(preference);
504        }
505    }
506
507    /**
508     * Sets the callback to be invoked when a {@link Preference} in the
509     * hierarchy rooted at this {@link PreferenceManager} is clicked.
510     *
511     * @param listener The callback to be invoked.
512     */
513    public void setOnPreferenceTreeClickListener(OnPreferenceTreeClickListener listener) {
514        mOnPreferenceTreeClickListener = listener;
515    }
516
517    public OnPreferenceTreeClickListener getOnPreferenceTreeClickListener() {
518        return mOnPreferenceTreeClickListener;
519    }
520
521    /**
522     * Sets the callback to be invoked when a {@link PreferenceScreen} in the hierarchy rooted at
523     * this {@link PreferenceManager} is clicked.
524     *
525     * @param listener The callback to be invoked.
526     */
527    public void setOnNavigateToScreenListener(OnNavigateToScreenListener listener) {
528        mOnNavigateToScreenListener = listener;
529    }
530
531    /**
532     * Returns the {@link PreferenceManager.OnNavigateToScreenListener}, if one has been set.
533     */
534    public OnNavigateToScreenListener getOnNavigateToScreenListener() {
535        return mOnNavigateToScreenListener;
536    }
537
538    /**
539     * Interface definition for a callback to be invoked when a
540     * {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is
541     * clicked.
542     */
543    public interface OnPreferenceTreeClickListener {
544        /**
545         * Called when a preference in the tree rooted at this
546         * {@link PreferenceScreen} has been clicked.
547         *
548         * @param preference The preference that was clicked.
549         * @return Whether the click was handled.
550         */
551        boolean onPreferenceTreeClick(Preference preference);
552    }
553
554    /**
555     * Interface definition for a class that will be called when a
556     * {@link android.support.v7.preference.Preference} requests to display a dialog.
557     */
558    public interface OnDisplayPreferenceDialogListener {
559
560        /**
561         * Called when a preference in the tree requests to display a dialog.
562         *
563         * @param preference The Preference object requesting the dialog.
564         */
565        void onDisplayPreferenceDialog(Preference preference);
566    }
567
568    /**
569     * Interface definition for a class that will be called when a
570     * {@link android.support.v7.preference.PreferenceScreen} requests navigation.
571     */
572    public interface OnNavigateToScreenListener {
573
574        /**
575         * Called when a PreferenceScreen in the tree requests to navigate to its contents.
576         *
577         * @param preferenceScreen The PreferenceScreen requesting navigation.
578         */
579        void onNavigateToScreen(PreferenceScreen preferenceScreen);
580    }
581
582}
583