/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.preference.PreferenceManager; import com.android.camera.debug.Log; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** * SettingsManager class provides an api for getting and setting SharedPreferences * values. * * Types * * This API simplifies settings type management by storing all settings values * in SharedPreferences as Strings. To do this, the API to converts boolean and * Integer values to Strings when those values are stored, making the conversion * back to a boolean or Integer also consistent and simple. * * This also enables the user to safely get settings values as three different types, * as it's convenient: String, Integer, and boolean values. Integers and boolean * can always be trivially converted to one another, but Strings cannot always be * parsed as Integers. In this case, if the user stores a String value that cannot * be parsed to an Integer yet they try to retrieve it as an Integer, the API throws * a meaningful exception to the user. * * Scope * * This API introduces the concept of "scope" for a setting, which is the generality * of a setting. The most general settings, that can be accessed acrossed the * entire application, have a scope of SCOPE_GLOBAL. They are stored in the default * SharedPreferences file. * * A setting that is local to a third party module or subset of the application has * a custom scope. The specific module can define whatever scope (String) argument * they want, and the settings saved with that scope can only be seen by that third * party module. Scope is a general concept that helps protect settings values * from being clobbered in different contexts. * * Keys and Defaults * * This API allows you to store your SharedPreferences keys and default values * outside the SettingsManager, because these values are either passed into * the API or stored in a cache when the user sets defaults. * * For any setting, it is optional to store a default or set of possible values, * unless you plan on using the getIndexOfCurrentValue and setValueByIndex, * methods, which rely on an index into the set of possible values. * */ @ThreadSafe public class SettingsManager { private static final Log.Tag TAG = new Log.Tag("SettingsManager"); private final Object mLock; private final Context mContext; private final String mPackageName; private final SharedPreferences mDefaultPreferences; private SharedPreferences mCustomPreferences; private final DefaultsStore mDefaultsStore = new DefaultsStore(); public static final String MODULE_SCOPE_PREFIX = "_preferences_module_"; public static final String CAMERA_SCOPE_PREFIX = "_preferences_camera_"; /** * A List of OnSettingChangedListener's, maintained to compare to new * listeners and prevent duplicate registering. */ private final List mListeners = new ArrayList(); /** * A List of OnSharedPreferenceChangeListener's, maintained to hold pointers * to actually registered listeners, so they can be unregistered. */ private final List mSharedPreferenceListeners = new ArrayList(); public SettingsManager(Context context) { mLock = new Object(); mContext = context; mPackageName = mContext.getPackageName(); mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); } /** * Get the SettingsManager's default preferences. This is useful * to third party modules as they are defining their upgrade paths, * since most third party modules will use either SCOPE_GLOBAL or a * custom scope. */ public SharedPreferences getDefaultPreferences() { synchronized (mLock) { return mDefaultPreferences; } } /** * Open a SharedPreferences file by custom scope. * Also registers any known SharedPreferenceListeners on this * SharedPreferences instance. */ protected SharedPreferences openPreferences(String scope) { synchronized (mLock) { SharedPreferences preferences; // For external camera, scope could have "/" separator which is a invalid path // for the shared preference. String validScope = scope.replaceAll("/", "_"); preferences = mContext.getSharedPreferences( mPackageName + validScope, Context.MODE_PRIVATE); for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) { preferences.registerOnSharedPreferenceChangeListener(listener); } return preferences; } } /** * Close a SharedPreferences file by custom scope. * The file isn't explicitly closed (the SharedPreferences API makes * this unnecessary), so the real work is to unregister any known * SharedPreferenceListeners from this SharedPreferences instance. * * It's important to do this as camera and modules change, because * we don't want old SharedPreferences listeners executing on * cameras/modules they are not compatible with. */ protected void closePreferences(SharedPreferences preferences) { synchronized (mLock) { for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) { preferences.unregisterOnSharedPreferenceChangeListener(listener); } } } public static String getCameraSettingScope(String cameraIdValue) { return CAMERA_SCOPE_PREFIX + cameraIdValue; } public static String getModuleSettingScope(String moduleScopeNamespace) { return CAMERA_SCOPE_PREFIX + moduleScopeNamespace; } /** * Interface with Camera Device Settings and Modules. */ public interface OnSettingChangedListener { /** * Called every time a SharedPreference has been changed. */ public void onSettingChanged(SettingsManager settingsManager, String key); } private OnSharedPreferenceChangeListener getSharedPreferenceListener( final OnSettingChangedListener listener) { return new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { listener.onSettingChanged(SettingsManager.this, key); } }; } /** * Add an OnSettingChangedListener to the SettingsManager, which will * execute onSettingsChanged when any SharedPreference has been updated. */ public void addListener(final OnSettingChangedListener listener) { synchronized (mLock) { if (listener == null) { throw new IllegalArgumentException("OnSettingChangedListener cannot be null."); } if (mListeners.contains(listener)) { return; } mListeners.add(listener); OnSharedPreferenceChangeListener sharedPreferenceListener = getSharedPreferenceListener(listener); mSharedPreferenceListeners.add(sharedPreferenceListener); mDefaultPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceListener); if (mCustomPreferences != null) { mCustomPreferences.registerOnSharedPreferenceChangeListener( sharedPreferenceListener); } Log.v(TAG, "listeners: " + mListeners); } } /** * Remove a specific SettingsListener. This should be done in onPause if a * listener has been set. */ public void removeListener(OnSettingChangedListener listener) { synchronized (mLock) { if (listener == null) { throw new IllegalArgumentException(); } if (!mListeners.contains(listener)) { return; } int index = mListeners.indexOf(listener); mListeners.remove(listener); OnSharedPreferenceChangeListener sharedPreferenceListener = mSharedPreferenceListeners.get(index); mSharedPreferenceListeners.remove(index); mDefaultPreferences.unregisterOnSharedPreferenceChangeListener( sharedPreferenceListener); if (mCustomPreferences != null) { mCustomPreferences.unregisterOnSharedPreferenceChangeListener( sharedPreferenceListener); } } } /** * Remove all OnSharedPreferenceChangedListener's. This should be done in * onDestroy. */ public void removeAllListeners() { synchronized (mLock) { for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) { mDefaultPreferences.unregisterOnSharedPreferenceChangeListener(listener); if (mCustomPreferences != null) { mCustomPreferences.unregisterOnSharedPreferenceChangeListener(listener); } } mSharedPreferenceListeners.clear(); mListeners.clear(); } } /** This scope stores and retrieves settings from default preferences. */ public static final String SCOPE_GLOBAL = "default_scope"; /** * Returns the SharedPreferences file matching the scope * argument. * * Camera and module preferences files are cached, * until the camera id or module id changes, then the listeners * are unregistered and a new file is opened. */ private SharedPreferences getPreferencesFromScope(String scope) { synchronized (mLock) { if (scope.equals(SCOPE_GLOBAL)) { return mDefaultPreferences; } if (mCustomPreferences != null) { closePreferences(mCustomPreferences); } mCustomPreferences = openPreferences(scope); return mCustomPreferences; } } /** * Set default and valid values for a setting, for a String default and * a set of String possible values that are already defined. * This is not required. */ public void setDefaults(String key, String defaultValue, String[] possibleValues) { synchronized (mLock) { mDefaultsStore.storeDefaults(key, defaultValue, possibleValues); } } /** * Set default and valid values for a setting, for an Integer default and * a set of Integer possible values that are already defined. * This is not required. */ public void setDefaults(String key, int defaultValue, int[] possibleValues) { synchronized (mLock) { String defaultValueString = Integer.toString(defaultValue); String[] possibleValuesString = new String[possibleValues.length]; for (int i = 0; i < possibleValues.length; i++) { possibleValuesString[i] = Integer.toString(possibleValues[i]); } mDefaultsStore.storeDefaults(key, defaultValueString, possibleValuesString); } } /** * Set default and valid values for a setting, for a boolean default. * The set of boolean possible values is always { false, true }. * This is not required. */ public void setDefaults(String key, boolean defaultValue) { synchronized (mLock) { String defaultValueString = defaultValue ? "1" : "0"; String[] possibleValues = {"0", "1"}; mDefaultsStore.storeDefaults(key, defaultValueString, possibleValues); } } /** * Retrieve a default from the DefaultsStore as a String. */ public String getStringDefault(String key) { synchronized (mLock) { return mDefaultsStore.getDefaultValue(key); } } /** * Retrieve a default from the DefaultsStore as an Integer. */ public Integer getIntegerDefault(String key) { synchronized (mLock) { String defaultValueString = mDefaultsStore.getDefaultValue(key); return defaultValueString == null ? 0 : Integer.parseInt(defaultValueString); } } /** * Retrieve a default from the DefaultsStore as a boolean. */ public boolean getBooleanDefault(String key) { synchronized (mLock) { String defaultValueString = mDefaultsStore.getDefaultValue(key); return defaultValueString == null ? false : (Integer.parseInt(defaultValueString) != 0); } } /** * Retrieve a setting's value as a String, manually specifiying * a default value. */ public String getString(String scope, String key, String defaultValue) { synchronized (mLock) { SharedPreferences preferences = getPreferencesFromScope(scope); try { return preferences.getString(key, defaultValue); } catch (ClassCastException e) { Log.w(TAG, "existing preference with invalid type, removing and returning default", e); preferences.edit().remove(key).apply(); return defaultValue; } } } /** * Retrieve a setting's value as a String, using the default value * stored in the DefaultsStore. */ @Nullable public String getString(String scope, String key) { synchronized (mLock) { return getString(scope, key, getStringDefault(key)); } } /** * Retrieve a setting's value as an Integer, manually specifying * a default value. */ public int getInteger(String scope, String key, Integer defaultValue) { synchronized (mLock) { String defaultValueString = Integer.toString(defaultValue); String value = getString(scope, key, defaultValueString); return convertToInt(value); } } /** * Retrieve a setting's value as an Integer, converting the default value * stored in the DefaultsStore. */ public int getInteger(String scope, String key) { synchronized (mLock) { return getInteger(scope, key, getIntegerDefault(key)); } } /** * Retrieve a setting's value as a boolean, manually specifiying * a default value. */ public boolean getBoolean(String scope, String key, boolean defaultValue) { synchronized (mLock) { String defaultValueString = defaultValue ? "1" : "0"; String value = getString(scope, key, defaultValueString); return convertToBoolean(value); } } /** * Retrieve a setting's value as a boolean, converting the default value * stored in the DefaultsStore. */ public boolean getBoolean(String scope, String key) { synchronized (mLock) { return getBoolean(scope, key, getBooleanDefault(key)); } } /** * If possible values are stored for this key, return the * index into that list of the currently set value. * * For example, if a set of possible values is [2,3,5], * and the current value set of this key is 3, this method * returns 1. * * If possible values are not stored for this key, throw * an IllegalArgumentException. */ public int getIndexOfCurrentValue(String scope, String key) { synchronized (mLock) { String[] possibleValues = mDefaultsStore.getPossibleValues(key); if (possibleValues == null || possibleValues.length == 0) { throw new IllegalArgumentException( "No possible values for scope=" + scope + " key=" + key); } String value = getString(scope, key); for (int i = 0; i < possibleValues.length; i++) { if (value.equals(possibleValues[i])) { return i; } } throw new IllegalStateException("Current value for scope=" + scope + " key=" + key + " not in list of possible values"); } } /** * Store a setting's value using a String value. No conversion * occurs before this value is stored in SharedPreferences. */ public void set(String scope, String key, String value) { synchronized (mLock) { SharedPreferences preferences = getPreferencesFromScope(scope); preferences.edit().putString(key, value).apply(); } } /** * Store a setting's value using an Integer value. Type conversion * to String occurs before this value is stored in SharedPreferences. */ public void set(String scope, String key, int value) { synchronized (mLock) { set(scope, key, convert(value)); } } /** * Store a setting's value using a boolean value. Type conversion * to an Integer and then to a String occurs before this value is * stored in SharedPreferences. */ public void set(String scope, String key, boolean value) { synchronized (mLock) { set(scope, key, convert(value)); } } /** * Set a setting to the default value stored in the DefaultsStore. */ public void setToDefault(String scope, String key) { synchronized (mLock) { set(scope, key, getStringDefault(key)); } } /** * If a set of possible values is defined, set the current value * of a setting to the possible value found at the given index. * * For example, if the possible values for a key are [2,3,5], * and the index given to this method is 2, then this method would * store the value 5 in SharedPreferences for the key. * * If the index is out of the bounds of the range of possible values, * or there are no possible values for this key, then this * method throws an exception. */ public void setValueByIndex(String scope, String key, int index) { synchronized (mLock) { String[] possibleValues = mDefaultsStore.getPossibleValues(key); if (possibleValues.length == 0) { throw new IllegalArgumentException( "No possible values for scope=" + scope + " key=" + key); } if (index >= 0 && index < possibleValues.length) { set(scope, key, possibleValues[index]); } else { throw new IndexOutOfBoundsException("For possible values of scope=" + scope + " key=" + key); } } } /** * Check that a setting has some value stored. */ public boolean isSet(String scope, String key) { synchronized (mLock) { SharedPreferences preferences = getPreferencesFromScope(scope); return preferences.contains(key); } } /** * Check whether a settings's value is currently set to the * default value. */ public boolean isDefault(String scope, String key) { synchronized (mLock) { String defaultValue = getStringDefault(key); String value = getString(scope, key); return value == null ? false : value.equals(defaultValue); } } /** * Remove a setting. */ public void remove(String scope, String key) { synchronized (mLock) { SharedPreferences preferences = getPreferencesFromScope(scope); preferences.edit().remove(key).apply(); } } /** * Package private conversion method to turn ints into preferred * String storage format. * * @param value int to be stored in Settings * @return String which represents the int */ static String convert(int value) { return Integer.toString(value); } /** * Package private conversion method to turn String storage format into * ints. * * @param value String to be converted to int * @return int value of stored String */ static int convertToInt(String value) { return Integer.parseInt(value); } /** * Package private conversion method to turn String storage format into * booleans. * * @param value String to be converted to boolean * @return boolean value of stored String */ static boolean convertToBoolean(String value) { return Integer.parseInt(value) != 0; } /** * Package private conversion method to turn booleans into preferred * String storage format. * * @param value boolean to be stored in Settings * @return String which represents the boolean */ static String convert(boolean value) { return value ? "1" : "0"; } }