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 
17 package com.android.camera.settings;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
22 import android.preference.PreferenceManager;
23 
24 import com.android.camera.debug.Log;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 
29 import javax.annotation.Nullable;
30 import 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
76 public 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 
SettingsManager(Context context)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      */
getDefaultPreferences()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      */
openPreferences(String scope)128     protected SharedPreferences openPreferences(String scope) {
129         synchronized (mLock) {
130             SharedPreferences preferences;
131             // For external camera, scope could have "/" separator which is a invalid path
132             // for the shared preference.
133             String validScope = scope.replaceAll("/", "_");
134             preferences = mContext.getSharedPreferences(
135                     mPackageName + validScope, Context.MODE_PRIVATE);
136 
137             for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
138                 preferences.registerOnSharedPreferenceChangeListener(listener);
139             }
140             return preferences;
141         }
142     }
143 
144     /**
145      * Close a SharedPreferences file by custom scope.
146      * The file isn't explicitly closed (the SharedPreferences API makes
147      * this unnecessary), so the real work is to unregister any known
148      * SharedPreferenceListeners from this SharedPreferences instance.
149      *
150      * It's important to do this as camera and modules change, because
151      * we don't want old SharedPreferences listeners executing on
152      * cameras/modules they are not compatible with.
153      */
closePreferences(SharedPreferences preferences)154     protected void closePreferences(SharedPreferences preferences) {
155         synchronized (mLock) {
156             for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
157                 preferences.unregisterOnSharedPreferenceChangeListener(listener);
158             }
159         }
160     }
161 
getCameraSettingScope(String cameraIdValue)162     public static String getCameraSettingScope(String cameraIdValue) {
163         return CAMERA_SCOPE_PREFIX + cameraIdValue;
164     }
165 
getModuleSettingScope(String moduleScopeNamespace)166     public static String getModuleSettingScope(String moduleScopeNamespace) {
167         return CAMERA_SCOPE_PREFIX + moduleScopeNamespace;
168     }
169 
170     /**
171      * Interface with Camera Device Settings and Modules.
172      */
173     public interface OnSettingChangedListener {
174         /**
175          * Called every time a SharedPreference has been changed.
176          */
onSettingChanged(SettingsManager settingsManager, String key)177         public void onSettingChanged(SettingsManager settingsManager, String key);
178     }
179 
getSharedPreferenceListener( final OnSettingChangedListener listener)180     private OnSharedPreferenceChangeListener getSharedPreferenceListener(
181             final OnSettingChangedListener listener) {
182         return new OnSharedPreferenceChangeListener() {
183             @Override
184             public void onSharedPreferenceChanged(
185                     SharedPreferences sharedPreferences, String key) {
186                 listener.onSettingChanged(SettingsManager.this, key);
187             }
188         };
189     }
190 
191     /**
192      * Add an OnSettingChangedListener to the SettingsManager, which will
193      * execute onSettingsChanged when any SharedPreference has been updated.
194      */
195     public void addListener(final OnSettingChangedListener listener) {
196         synchronized (mLock) {
197             if (listener == null) {
198                 throw new IllegalArgumentException("OnSettingChangedListener cannot be null.");
199             }
200 
201             if (mListeners.contains(listener)) {
202                 return;
203             }
204 
205             mListeners.add(listener);
206             OnSharedPreferenceChangeListener sharedPreferenceListener =
207                     getSharedPreferenceListener(listener);
208             mSharedPreferenceListeners.add(sharedPreferenceListener);
209             mDefaultPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceListener);
210 
211             if (mCustomPreferences != null) {
212                 mCustomPreferences.registerOnSharedPreferenceChangeListener(
213                         sharedPreferenceListener);
214             }
215             Log.v(TAG, "listeners: " + mListeners);
216         }
217     }
218 
219     /**
220      * Remove a specific SettingsListener. This should be done in onPause if a
221      * listener has been set.
222      */
223     public void removeListener(OnSettingChangedListener listener) {
224         synchronized (mLock) {
225             if (listener == null) {
226                 throw new IllegalArgumentException();
227             }
228 
229             if (!mListeners.contains(listener)) {
230                 return;
231             }
232 
233             int index = mListeners.indexOf(listener);
234             mListeners.remove(listener);
235 
236             OnSharedPreferenceChangeListener sharedPreferenceListener =
237                     mSharedPreferenceListeners.get(index);
238             mSharedPreferenceListeners.remove(index);
239             mDefaultPreferences.unregisterOnSharedPreferenceChangeListener(
240                     sharedPreferenceListener);
241 
242             if (mCustomPreferences != null) {
243                 mCustomPreferences.unregisterOnSharedPreferenceChangeListener(
244                         sharedPreferenceListener);
245             }
246         }
247     }
248 
249     /**
250      * Remove all OnSharedPreferenceChangedListener's. This should be done in
251      * onDestroy.
252      */
253     public void removeAllListeners() {
254         synchronized (mLock) {
255             for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) {
256                 mDefaultPreferences.unregisterOnSharedPreferenceChangeListener(listener);
257 
258                 if (mCustomPreferences != null) {
259                     mCustomPreferences.unregisterOnSharedPreferenceChangeListener(listener);
260                 }
261             }
262             mSharedPreferenceListeners.clear();
263             mListeners.clear();
264         }
265     }
266 
267     /** This scope stores and retrieves settings from
268         default preferences. */
269     public static final String SCOPE_GLOBAL = "default_scope";
270 
271     /**
272      * Returns the SharedPreferences file matching the scope
273      * argument.
274      *
275      * Camera and module preferences files are cached,
276      * until the camera id or module id changes, then the listeners
277      * are unregistered and a new file is opened.
278      */
279     private SharedPreferences getPreferencesFromScope(String scope) {
280         synchronized (mLock) {
281             if (scope.equals(SCOPE_GLOBAL)) {
282                 return mDefaultPreferences;
283             }
284 
285             if (mCustomPreferences != null) {
286                 closePreferences(mCustomPreferences);
287             }
288             mCustomPreferences = openPreferences(scope);
289             return mCustomPreferences;
290         }
291     }
292 
293     /**
294      * Set default and valid values for a setting, for a String default and
295      * a set of String possible values that are already defined.
296      * This is not required.
297      */
298     public void setDefaults(String key, String defaultValue, String[] possibleValues) {
299         synchronized (mLock) {
300             mDefaultsStore.storeDefaults(key, defaultValue, possibleValues);
301         }
302     }
303 
304     /**
305      * Set default and valid values for a setting, for an Integer default and
306      * a set of Integer possible values that are already defined.
307      * This is not required.
308      */
309     public void setDefaults(String key, int defaultValue, int[] possibleValues) {
310         synchronized (mLock) {
311             String defaultValueString = Integer.toString(defaultValue);
312             String[] possibleValuesString = new String[possibleValues.length];
313             for (int i = 0; i < possibleValues.length; i++) {
314                 possibleValuesString[i] = Integer.toString(possibleValues[i]);
315             }
316             mDefaultsStore.storeDefaults(key, defaultValueString, possibleValuesString);
317         }
318     }
319 
320     /**
321      * Set default and valid values for a setting, for a boolean default.
322      * The set of boolean possible values is always { false, true }.
323      * This is not required.
324      */
325     public void setDefaults(String key, boolean defaultValue) {
326         synchronized (mLock) {
327             String defaultValueString = defaultValue ? "1" : "0";
328             String[] possibleValues = {"0", "1"};
329             mDefaultsStore.storeDefaults(key, defaultValueString, possibleValues);
330         }
331     }
332 
333     /**
334      * Retrieve a default from the DefaultsStore as a String.
335      */
336     public String getStringDefault(String key) {
337         synchronized (mLock) {
338             return mDefaultsStore.getDefaultValue(key);
339         }
340     }
341 
342     /**
343      * Retrieve a default from the DefaultsStore as an Integer.
344      */
345     public Integer getIntegerDefault(String key) {
346         synchronized (mLock) {
347             String defaultValueString = mDefaultsStore.getDefaultValue(key);
348             return defaultValueString == null ? 0 : Integer.parseInt(defaultValueString);
349         }
350     }
351 
352     /**
353      * Retrieve a default from the DefaultsStore as a boolean.
354      */
355     public boolean getBooleanDefault(String key) {
356         synchronized (mLock) {
357             String defaultValueString = mDefaultsStore.getDefaultValue(key);
358             return defaultValueString == null ? false :
359                     (Integer.parseInt(defaultValueString) != 0);
360         }
361     }
362 
363     /**
364      * Retrieve a setting's value as a String, manually specifiying
365      * a default value.
366      */
367     public String getString(String scope, String key, String defaultValue) {
368         synchronized (mLock) {
369             SharedPreferences preferences = getPreferencesFromScope(scope);
370             try {
371                 return preferences.getString(key, defaultValue);
372             } catch (ClassCastException e) {
373                 Log.w(TAG, "existing preference with invalid type, removing and returning default", e);
374                 preferences.edit().remove(key).apply();
375                 return defaultValue;
376             }
377         }
378     }
379 
380     /**
381      * Retrieve a setting's value as a String, using the default value
382      * stored in the DefaultsStore.
383      */
384     @Nullable
385     public String getString(String scope, String key) {
386         synchronized (mLock) {
387             return getString(scope, key, getStringDefault(key));
388         }
389     }
390 
391     /**
392      * Retrieve a setting's value as an Integer, manually specifying
393      * a default value.
394      */
395     public int getInteger(String scope, String key, Integer defaultValue) {
396         synchronized (mLock) {
397             String defaultValueString = Integer.toString(defaultValue);
398             String value = getString(scope, key, defaultValueString);
399             return convertToInt(value);
400         }
401     }
402 
403     /**
404      * Retrieve a setting's value as an Integer, converting the default value
405      * stored in the DefaultsStore.
406      */
407     public int getInteger(String scope, String key) {
408         synchronized (mLock) {
409             return getInteger(scope, key, getIntegerDefault(key));
410         }
411     }
412 
413     /**
414      * Retrieve a setting's value as a boolean, manually specifiying
415      * a default value.
416      */
417     public boolean getBoolean(String scope, String key, boolean defaultValue) {
418         synchronized (mLock) {
419             String defaultValueString = defaultValue ? "1" : "0";
420             String value = getString(scope, key, defaultValueString);
421             return convertToBoolean(value);
422         }
423     }
424 
425     /**
426      * Retrieve a setting's value as a boolean, converting the default value
427      * stored in the DefaultsStore.
428      */
429     public boolean getBoolean(String scope, String key) {
430         synchronized (mLock) {
431             return getBoolean(scope, key, getBooleanDefault(key));
432         }
433     }
434 
435     /**
436      * If possible values are stored for this key, return the
437      * index into that list of the currently set value.
438      *
439      * For example, if a set of possible values is [2,3,5],
440      * and the current value set of this key is 3, this method
441      * returns 1.
442      *
443      * If possible values are not stored for this key, throw
444      * an IllegalArgumentException.
445      */
446     public int getIndexOfCurrentValue(String scope, String key) {
447         synchronized (mLock) {
448             String[] possibleValues = mDefaultsStore.getPossibleValues(key);
449             if (possibleValues == null || possibleValues.length == 0) {
450                 throw new IllegalArgumentException(
451                         "No possible values for scope=" + scope + " key=" + key);
452             }
453 
454             String value = getString(scope, key);
455             for (int i = 0; i < possibleValues.length; i++) {
456                 if (value.equals(possibleValues[i])) {
457                     return i;
458                 }
459             }
460             throw new IllegalStateException("Current value for scope=" + scope + " key="
461                     + key + " not in list of possible values");
462         }
463     }
464 
465     /**
466      * Store a setting's value using a String value.  No conversion
467      * occurs before this value is stored in SharedPreferences.
468      */
469     public void set(String scope, String key, String value) {
470         synchronized (mLock) {
471             SharedPreferences preferences = getPreferencesFromScope(scope);
472             preferences.edit().putString(key, value).apply();
473         }
474     }
475 
476     /**
477      * Store a setting's value using an Integer value.  Type conversion
478      * to String occurs before this value is stored in SharedPreferences.
479      */
480     public void set(String scope, String key, int value) {
481         synchronized (mLock) {
482             set(scope, key, convert(value));
483         }
484     }
485 
486     /**
487      * Store a setting's value using a boolean value.  Type conversion
488      * to an Integer and then to a String occurs before this value is
489      * stored in SharedPreferences.
490      */
491     public void set(String scope, String key, boolean value) {
492         synchronized (mLock) {
493             set(scope, key, convert(value));
494         }
495     }
496 
497     /**
498      * Set a setting to the default value stored in the DefaultsStore.
499      */
500     public void setToDefault(String scope, String key) {
501         synchronized (mLock) {
502             set(scope, key, getStringDefault(key));
503         }
504     }
505 
506     /**
507      * If a set of possible values is defined, set the current value
508      * of a setting to the possible value found at the given index.
509      *
510      * For example, if the possible values for a key are [2,3,5],
511      * and the index given to this method is 2, then this method would
512      * store the value 5 in SharedPreferences for the key.
513      *
514      * If the index is out of the bounds of the range of possible values,
515      * or there are no possible values for this key, then this
516      * method throws an exception.
517      */
518     public void setValueByIndex(String scope, String key, int index) {
519         synchronized (mLock) {
520             String[] possibleValues = mDefaultsStore.getPossibleValues(key);
521             if (possibleValues.length == 0) {
522                 throw new IllegalArgumentException(
523                         "No possible values for scope=" + scope + " key=" + key);
524             }
525 
526             if (index >= 0 && index < possibleValues.length) {
527                 set(scope, key, possibleValues[index]);
528             } else {
529                 throw new IndexOutOfBoundsException("For possible values of scope=" + scope
530                         + " key=" + key);
531             }
532         }
533     }
534 
535     /**
536      * Check that a setting has some value stored.
537      */
538     public boolean isSet(String scope, String key) {
539         synchronized (mLock) {
540             SharedPreferences preferences = getPreferencesFromScope(scope);
541             return preferences.contains(key);
542         }
543     }
544 
545     /**
546      * Check whether a settings's value is currently set to the
547      * default value.
548      */
549     public boolean isDefault(String scope, String key) {
550         synchronized (mLock) {
551             String defaultValue = getStringDefault(key);
552             String value = getString(scope, key);
553             return value == null ? false : value.equals(defaultValue);
554         }
555     }
556 
557     /**
558      * Remove a setting.
559      */
560     public void remove(String scope, String key) {
561         synchronized (mLock) {
562             SharedPreferences preferences = getPreferencesFromScope(scope);
563             preferences.edit().remove(key).apply();
564         }
565     }
566 
567     /**
568      * Package private conversion method to turn ints into preferred
569      * String storage format.
570      *
571      * @param value int to be stored in Settings
572      * @return String which represents the int
573      */
574     static String convert(int value) {
575         return Integer.toString(value);
576     }
577 
578     /**
579      * Package private conversion method to turn String storage format into
580      * ints.
581      *
582      * @param value String to be converted to int
583      * @return int value of stored String
584      */
585     static int convertToInt(String value) {
586         return Integer.parseInt(value);
587     }
588 
589     /**
590      * Package private conversion method to turn String storage format into
591      * booleans.
592      *
593      * @param value String to be converted to boolean
594      * @return boolean value of stored String
595      */
596     static boolean convertToBoolean(String value) {
597         return Integer.parseInt(value) != 0;
598     }
599 
600 
601     /**
602      * Package private conversion method to turn booleans into preferred
603      * String storage format.
604      *
605      * @param value boolean to be stored in Settings
606      * @return String which represents the boolean
607      */
608     static String convert(boolean value) {
609         return value ? "1" : "0";
610     }
611 }
612