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