1/*
2 * Copyright (C) 2013 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 com.android.camera.settings;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
22import android.preference.PreferenceManager;
23
24import com.android.camera.debug.Log;
25
26import java.util.ArrayList;
27import java.util.List;
28
29import javax.annotation.Nullable;
30import javax.annotation.concurrent.ThreadSafe;
31
32
33/**
34 * SettingsManager class provides an api for getting and setting SharedPreferences
35 * values.
36 *
37 * Types
38 *
39 * This API simplifies settings type management by storing all settings values
40 * in SharedPreferences as Strings.  To do this, the API to converts boolean and
41 * Integer values to Strings when those values are stored, making the conversion
42 * back to a boolean or Integer also consistent and simple.
43 *
44 * This also enables the user to safely get settings values as three different types,
45 * as it's convenient: String, Integer, and boolean values.  Integers and boolean
46 * can always be trivially converted to one another, but Strings cannot always be
47 * parsed as Integers.  In this case, if the user stores a String value that cannot
48 * be parsed to an Integer yet they try to retrieve it as an Integer, the API throws
49 * a meaningful exception to the user.
50 *
51 * Scope
52 *
53 * This API introduces the concept of "scope" for a setting, which is the generality
54 * of a setting.  The most general settings, that can be accessed acrossed the
55 * entire application, have a scope of SCOPE_GLOBAL.  They are stored in the default
56 * SharedPreferences file.
57 *
58 * A setting that is local to a third party module or subset of the application has
59 * a custom scope.  The specific module can define whatever scope (String) argument
60 * they want, and the settings saved with that scope can only be seen by that third
61 * party module.  Scope is a general concept that helps protect settings values
62 * from being clobbered in different contexts.
63 *
64 * Keys and Defaults
65 *
66 * This API allows you to store your SharedPreferences keys and default values
67 * outside the SettingsManager, because these values are either passed into
68 * the API or stored in a cache when the user sets defaults.
69 *
70 * For any setting, it is optional to store a default or set of possible values,
71 * unless you plan on using the getIndexOfCurrentValue and setValueByIndex,
72 * methods, which rely on an index into the set of possible values.
73 *
74 */
75@ThreadSafe
76public class SettingsManager {
77    private static final Log.Tag TAG = new Log.Tag("SettingsManager");
78
79    private final Object mLock;
80    private final Context mContext;
81    private final String mPackageName;
82    private final SharedPreferences mDefaultPreferences;
83    private SharedPreferences mCustomPreferences;
84    private final DefaultsStore mDefaultsStore = new DefaultsStore();
85
86    public static final String MODULE_SCOPE_PREFIX = "_preferences_module_";
87    public static final String CAMERA_SCOPE_PREFIX = "_preferences_camera_";
88
89    /**
90     * A List of OnSettingChangedListener's, maintained to compare to new
91     * listeners and prevent duplicate registering.
92     */
93    private final List<OnSettingChangedListener> mListeners =
94        new ArrayList<OnSettingChangedListener>();
95
96    /**
97     * A List of OnSharedPreferenceChangeListener's, maintained to hold pointers
98     * to actually registered listeners, so they can be unregistered.
99     */
100    private final List<OnSharedPreferenceChangeListener> mSharedPreferenceListeners =
101        new ArrayList<OnSharedPreferenceChangeListener>();
102
103    public SettingsManager(Context context) {
104        mLock = new Object();
105        mContext = context;
106        mPackageName = mContext.getPackageName();
107
108        mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
109    }
110
111    /**
112     * Get the SettingsManager's default preferences.  This is useful
113     * to third party modules as they are defining their upgrade paths,
114     * since most third party modules will use either SCOPE_GLOBAL or a
115     * custom scope.
116     */
117    public SharedPreferences getDefaultPreferences() {
118        synchronized (mLock) {
119            return mDefaultPreferences;
120        }
121    }
122
123    /**
124     * Open a SharedPreferences file by custom scope.
125     * Also registers any known SharedPreferenceListeners on this
126     * SharedPreferences instance.
127     */
128    protected SharedPreferences openPreferences(String scope) {
129        synchronized (mLock) {
130            SharedPreferences preferences;
131            preferences = mContext.getSharedPreferences(
132                    mPackageName + scope, Context.MODE_PRIVATE);
133
134            for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
135                preferences.registerOnSharedPreferenceChangeListener(listener);
136            }
137            return preferences;
138        }
139    }
140
141    /**
142     * Close a SharedPreferences file by custom scope.
143     * The file isn't explicitly closed (the SharedPreferences API makes
144     * this unnecessary), so the real work is to unregister any known
145     * SharedPreferenceListeners from this SharedPreferences instance.
146     *
147     * It's important to do this as camera and modules change, because
148     * we don't want old SharedPreferences listeners executing on
149     * cameras/modules they are not compatible with.
150     */
151    protected void closePreferences(SharedPreferences preferences) {
152        synchronized (mLock) {
153            for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
154                preferences.unregisterOnSharedPreferenceChangeListener(listener);
155            }
156        }
157    }
158
159    public static String getCameraSettingScope(String cameraIdValue) {
160        return CAMERA_SCOPE_PREFIX + cameraIdValue;
161    }
162
163    public static String getModuleSettingScope(String moduleScopeNamespace) {
164        return CAMERA_SCOPE_PREFIX + moduleScopeNamespace;
165    }
166
167    /**
168     * Interface with Camera Device Settings and Modules.
169     */
170    public interface OnSettingChangedListener {
171        /**
172         * Called every time a SharedPreference has been changed.
173         */
174        public void onSettingChanged(SettingsManager settingsManager, String key);
175    }
176
177    private OnSharedPreferenceChangeListener getSharedPreferenceListener(
178            final OnSettingChangedListener listener) {
179        return new OnSharedPreferenceChangeListener() {
180            @Override
181            public void onSharedPreferenceChanged(
182                    SharedPreferences sharedPreferences, String key) {
183                listener.onSettingChanged(SettingsManager.this, key);
184            }
185        };
186    }
187
188    /**
189     * Add an OnSettingChangedListener to the SettingsManager, which will
190     * execute onSettingsChanged when any SharedPreference has been updated.
191     */
192    public void addListener(final OnSettingChangedListener listener) {
193        synchronized (mLock) {
194            if (listener == null) {
195                throw new IllegalArgumentException("OnSettingChangedListener cannot be null.");
196            }
197
198            if (mListeners.contains(listener)) {
199                return;
200            }
201
202            mListeners.add(listener);
203            OnSharedPreferenceChangeListener sharedPreferenceListener =
204                    getSharedPreferenceListener(listener);
205            mSharedPreferenceListeners.add(sharedPreferenceListener);
206            mDefaultPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceListener);
207
208            if (mCustomPreferences != null) {
209                mCustomPreferences.registerOnSharedPreferenceChangeListener(
210                        sharedPreferenceListener);
211            }
212            Log.v(TAG, "listeners: " + mListeners);
213        }
214    }
215
216    /**
217     * Remove a specific SettingsListener. This should be done in onPause if a
218     * listener has been set.
219     */
220    public void removeListener(OnSettingChangedListener listener) {
221        synchronized (mLock) {
222            if (listener == null) {
223                throw new IllegalArgumentException();
224            }
225
226            if (!mListeners.contains(listener)) {
227                return;
228            }
229
230            int index = mListeners.indexOf(listener);
231            mListeners.remove(listener);
232
233            OnSharedPreferenceChangeListener sharedPreferenceListener =
234                    mSharedPreferenceListeners.get(index);
235            mSharedPreferenceListeners.remove(index);
236            mDefaultPreferences.unregisterOnSharedPreferenceChangeListener(
237                    sharedPreferenceListener);
238
239            if (mCustomPreferences != null) {
240                mCustomPreferences.unregisterOnSharedPreferenceChangeListener(
241                        sharedPreferenceListener);
242            }
243        }
244    }
245
246    /**
247     * Remove all OnSharedPreferenceChangedListener's. This should be done in
248     * onDestroy.
249     */
250    public void removeAllListeners() {
251        synchronized (mLock) {
252            for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
253                mDefaultPreferences.unregisterOnSharedPreferenceChangeListener(listener);
254
255                if (mCustomPreferences != null) {
256                    mCustomPreferences.unregisterOnSharedPreferenceChangeListener(listener);
257                }
258            }
259            mSharedPreferenceListeners.clear();
260            mListeners.clear();
261        }
262    }
263
264    /** This scope stores and retrieves settings from
265        default preferences. */
266    public static final String SCOPE_GLOBAL = "default_scope";
267
268    /**
269     * Returns the SharedPreferences file matching the scope
270     * argument.
271     *
272     * Camera and module preferences files are cached,
273     * until the camera id or module id changes, then the listeners
274     * are unregistered and a new file is opened.
275     */
276    private SharedPreferences getPreferencesFromScope(String scope) {
277        synchronized (mLock) {
278            if (scope.equals(SCOPE_GLOBAL)) {
279                return mDefaultPreferences;
280            }
281
282            if (mCustomPreferences != null) {
283                closePreferences(mCustomPreferences);
284            }
285            mCustomPreferences = openPreferences(scope);
286            return mCustomPreferences;
287        }
288    }
289
290    /**
291     * Set default and valid values for a setting, for a String default and
292     * a set of String possible values that are already defined.
293     * This is not required.
294     */
295    public void setDefaults(String key, String defaultValue, String[] possibleValues) {
296        synchronized (mLock) {
297            mDefaultsStore.storeDefaults(key, defaultValue, possibleValues);
298        }
299    }
300
301    /**
302     * Set default and valid values for a setting, for an Integer default and
303     * a set of Integer possible values that are already defined.
304     * This is not required.
305     */
306    public void setDefaults(String key, int defaultValue, int[] possibleValues) {
307        synchronized (mLock) {
308            String defaultValueString = Integer.toString(defaultValue);
309            String[] possibleValuesString = new String[possibleValues.length];
310            for (int i = 0; i < possibleValues.length; i++) {
311                possibleValuesString[i] = Integer.toString(possibleValues[i]);
312            }
313            mDefaultsStore.storeDefaults(key, defaultValueString, possibleValuesString);
314        }
315    }
316
317    /**
318     * Set default and valid values for a setting, for a boolean default.
319     * The set of boolean possible values is always { false, true }.
320     * This is not required.
321     */
322    public void setDefaults(String key, boolean defaultValue) {
323        synchronized (mLock) {
324            String defaultValueString = defaultValue ? "1" : "0";
325            String[] possibleValues = {"0", "1"};
326            mDefaultsStore.storeDefaults(key, defaultValueString, possibleValues);
327        }
328    }
329
330    /**
331     * Retrieve a default from the DefaultsStore as a String.
332     */
333    public String getStringDefault(String key) {
334        synchronized (mLock) {
335            return mDefaultsStore.getDefaultValue(key);
336        }
337    }
338
339    /**
340     * Retrieve a default from the DefaultsStore as an Integer.
341     */
342    public Integer getIntegerDefault(String key) {
343        synchronized (mLock) {
344            String defaultValueString = mDefaultsStore.getDefaultValue(key);
345            return defaultValueString == null ? 0 : Integer.parseInt(defaultValueString);
346        }
347    }
348
349    /**
350     * Retrieve a default from the DefaultsStore as a boolean.
351     */
352    public boolean getBooleanDefault(String key) {
353        synchronized (mLock) {
354            String defaultValueString = mDefaultsStore.getDefaultValue(key);
355            return defaultValueString == null ? false :
356                    (Integer.parseInt(defaultValueString) != 0);
357        }
358    }
359
360    /**
361     * Retrieve a setting's value as a String, manually specifiying
362     * a default value.
363     */
364    public String getString(String scope, String key, String defaultValue) {
365        synchronized (mLock) {
366            SharedPreferences preferences = getPreferencesFromScope(scope);
367            try {
368                return preferences.getString(key, defaultValue);
369            } catch (ClassCastException e) {
370                Log.w(TAG, "existing preference with invalid type, removing and returning default", e);
371                preferences.edit().remove(key).apply();
372                return defaultValue;
373            }
374        }
375    }
376
377    /**
378     * Retrieve a setting's value as a String, using the default value
379     * stored in the DefaultsStore.
380     */
381    @Nullable
382    public String getString(String scope, String key) {
383        synchronized (mLock) {
384            return getString(scope, key, getStringDefault(key));
385        }
386    }
387
388    /**
389     * Retrieve a setting's value as an Integer, manually specifying
390     * a default value.
391     */
392    public int getInteger(String scope, String key, Integer defaultValue) {
393        synchronized (mLock) {
394            String defaultValueString = Integer.toString(defaultValue);
395            String value = getString(scope, key, defaultValueString);
396            return convertToInt(value);
397        }
398    }
399
400    /**
401     * Retrieve a setting's value as an Integer, converting the default value
402     * stored in the DefaultsStore.
403     */
404    public int getInteger(String scope, String key) {
405        synchronized (mLock) {
406            return getInteger(scope, key, getIntegerDefault(key));
407        }
408    }
409
410    /**
411     * Retrieve a setting's value as a boolean, manually specifiying
412     * a default value.
413     */
414    public boolean getBoolean(String scope, String key, boolean defaultValue) {
415        synchronized (mLock) {
416            String defaultValueString = defaultValue ? "1" : "0";
417            String value = getString(scope, key, defaultValueString);
418            return convertToBoolean(value);
419        }
420    }
421
422    /**
423     * Retrieve a setting's value as a boolean, converting the default value
424     * stored in the DefaultsStore.
425     */
426    public boolean getBoolean(String scope, String key) {
427        synchronized (mLock) {
428            return getBoolean(scope, key, getBooleanDefault(key));
429        }
430    }
431
432    /**
433     * If possible values are stored for this key, return the
434     * index into that list of the currently set value.
435     *
436     * For example, if a set of possible values is [2,3,5],
437     * and the current value set of this key is 3, this method
438     * returns 1.
439     *
440     * If possible values are not stored for this key, throw
441     * an IllegalArgumentException.
442     */
443    public int getIndexOfCurrentValue(String scope, String key) {
444        synchronized (mLock) {
445            String[] possibleValues = mDefaultsStore.getPossibleValues(key);
446            if (possibleValues == null || possibleValues.length == 0) {
447                throw new IllegalArgumentException(
448                        "No possible values for scope=" + scope + " key=" + key);
449            }
450
451            String value = getString(scope, key);
452            for (int i = 0; i < possibleValues.length; i++) {
453                if (value.equals(possibleValues[i])) {
454                    return i;
455                }
456            }
457            throw new IllegalStateException("Current value for scope=" + scope + " key="
458                    + key + " not in list of possible values");
459        }
460    }
461
462    /**
463     * Store a setting's value using a String value.  No conversion
464     * occurs before this value is stored in SharedPreferences.
465     */
466    public void set(String scope, String key, String value) {
467        synchronized (mLock) {
468            SharedPreferences preferences = getPreferencesFromScope(scope);
469            preferences.edit().putString(key, value).apply();
470        }
471    }
472
473    /**
474     * Store a setting's value using an Integer value.  Type conversion
475     * to String occurs before this value is stored in SharedPreferences.
476     */
477    public void set(String scope, String key, int value) {
478        synchronized (mLock) {
479            set(scope, key, convert(value));
480        }
481    }
482
483    /**
484     * Store a setting's value using a boolean value.  Type conversion
485     * to an Integer and then to a String occurs before this value is
486     * stored in SharedPreferences.
487     */
488    public void set(String scope, String key, boolean value) {
489        synchronized (mLock) {
490            set(scope, key, convert(value));
491        }
492    }
493
494    /**
495     * Set a setting to the default value stored in the DefaultsStore.
496     */
497    public void setToDefault(String scope, String key) {
498        synchronized (mLock) {
499            set(scope, key, getStringDefault(key));
500        }
501    }
502
503    /**
504     * If a set of possible values is defined, set the current value
505     * of a setting to the possible value found at the given index.
506     *
507     * For example, if the possible values for a key are [2,3,5],
508     * and the index given to this method is 2, then this method would
509     * store the value 5 in SharedPreferences for the key.
510     *
511     * If the index is out of the bounds of the range of possible values,
512     * or there are no possible values for this key, then this
513     * method throws an exception.
514     */
515    public void setValueByIndex(String scope, String key, int index) {
516        synchronized (mLock) {
517            String[] possibleValues = mDefaultsStore.getPossibleValues(key);
518            if (possibleValues.length == 0) {
519                throw new IllegalArgumentException(
520                        "No possible values for scope=" + scope + " key=" + key);
521            }
522
523            if (index >= 0 && index < possibleValues.length) {
524                set(scope, key, possibleValues[index]);
525            } else {
526                throw new IndexOutOfBoundsException("For possible values of scope=" + scope
527                        + " key=" + key);
528            }
529        }
530    }
531
532    /**
533     * Check that a setting has some value stored.
534     */
535    public boolean isSet(String scope, String key) {
536        synchronized (mLock) {
537            SharedPreferences preferences = getPreferencesFromScope(scope);
538            return preferences.contains(key);
539        }
540    }
541
542    /**
543     * Check whether a settings's value is currently set to the
544     * default value.
545     */
546    public boolean isDefault(String scope, String key) {
547        synchronized (mLock) {
548            String defaultValue = getStringDefault(key);
549            String value = getString(scope, key);
550            return value == null ? false : value.equals(defaultValue);
551        }
552    }
553
554    /**
555     * Remove a setting.
556     */
557    public void remove(String scope, String key) {
558        synchronized (mLock) {
559            SharedPreferences preferences = getPreferencesFromScope(scope);
560            preferences.edit().remove(key).apply();
561        }
562    }
563
564    /**
565     * Package private conversion method to turn ints into preferred
566     * String storage format.
567     *
568     * @param value int to be stored in Settings
569     * @return String which represents the int
570     */
571    static String convert(int value) {
572        return Integer.toString(value);
573    }
574
575    /**
576     * Package private conversion method to turn String storage format into
577     * ints.
578     *
579     * @param value String to be converted to int
580     * @return int value of stored String
581     */
582    static int convertToInt(String value) {
583        return Integer.parseInt(value);
584    }
585
586    /**
587     * Package private conversion method to turn String storage format into
588     * booleans.
589     *
590     * @param value String to be converted to boolean
591     * @return boolean value of stored String
592     */
593    static boolean convertToBoolean(String value) {
594        return Integer.parseInt(value) != 0;
595    }
596
597
598    /**
599     * Package private conversion method to turn booleans into preferred
600     * String storage format.
601     *
602     * @param value boolean to be stored in Settings
603     * @return String which represents the boolean
604     */
605    static String convert(boolean value) {
606        return value ? "1" : "0";
607    }
608}
609