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 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 */ closePreferences(SharedPreferences preferences)151 protected void closePreferences(SharedPreferences preferences) { 152 synchronized (mLock) { 153 for (OnSharedPreferenceChangeListener listener : mSharedPreferenceListeners) { 154 preferences.unregisterOnSharedPreferenceChangeListener(listener); 155 } 156 } 157 } 158 getCameraSettingScope(String cameraIdValue)159 public static String getCameraSettingScope(String cameraIdValue) { 160 return CAMERA_SCOPE_PREFIX + cameraIdValue; 161 } 162 getModuleSettingScope(String moduleScopeNamespace)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 */ onSettingChanged(SettingsManager settingsManager, String key)174 public void onSettingChanged(SettingsManager settingsManager, String key); 175 } 176 getSharedPreferenceListener( final OnSettingChangedListener listener)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