From fe2b7eae7dedd0ae70f60a74d6759ceaae4b6886 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Thu, 23 Mar 2017 14:38:16 -0700 Subject: [PATCH 01/16] Add settings module for android --- Libraries/Settings/Settings.android.js | 34 ---- .../Settings/{Settings.ios.js => Settings.js} | 41 ++++- Libraries/Settings/__tests__/Settings-test.js | 46 +++++ .../modules/settings/SettingsModule.java | 166 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 7 + 5 files changed, 251 insertions(+), 43 deletions(-) delete mode 100644 Libraries/Settings/Settings.android.js rename Libraries/Settings/{Settings.ios.js => Settings.js} (55%) create mode 100644 Libraries/Settings/__tests__/Settings-test.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java diff --git a/Libraries/Settings/Settings.android.js b/Libraries/Settings/Settings.android.js deleted file mode 100644 index d13a32ba9218..000000000000 --- a/Libraries/Settings/Settings.android.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule Settings - * @flow - */ -'use strict'; - -var Settings = { - get(key: string): mixed { - console.warn('Settings is not yet supported on Android'); - return null; - }, - - set(settings: Object) { - console.warn('Settings is not yet supported on Android'); - }, - - watchKeys(keys: string | Array, callback: Function): number { - console.warn('Settings is not yet supported on Android'); - return -1; - }, - - clearWatch(watchId: number) { - console.warn('Settings is not yet supported on Android'); - }, -}; - -module.exports = Settings; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.js similarity index 55% rename from Libraries/Settings/Settings.ios.js rename to Libraries/Settings/Settings.js index beda544b43e5..5752322c7777 100644 --- a/Libraries/Settings/Settings.ios.js +++ b/Libraries/Settings/Settings.js @@ -11,25 +11,44 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTSettingsManager = require('NativeModules').SettingsManager; +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const RCTSettingsManager = require('NativeModules').SettingsManager; -var invariant = require('fbjs/lib/invariant'); +const invariant = require('fbjs/lib/invariant'); -var subscriptions: Array<{keys: Array, callback: ?Function}> = []; +const subscriptions: Array<{keys: Array; callback: ?Function}> = []; -var Settings = { +/** + * @class + * @description + * An interface to the native "preferences" file storage that uses simple key-value pairs + * + */ +const Settings = { _settings: RCTSettingsManager && RCTSettingsManager.settings, get(key: string): mixed { return this._settings[key]; }, - set(settings: Object) { + /** + * Sets the value for a name-value pairs + * @param settings the object with the name/value pairs to set + * + * Note that on android the only allowed value types are number, string and boolean + */ + set(settings: Object): void { this._settings = Object.assign(this._settings, settings); RCTSettingsManager.setValues(settings); }, + + /** + * Monitor one or more keys for changed values + * @param keys a string or an array of strings naming the keys to monitor + * @callback the callback to invoke when one of the specified keys updates its value + * @returns a number representing the watch ID which can be used to clear the watch + */ watchKeys(keys: string | Array, callback: Function): number { if (typeof keys === 'string') { keys = [keys]; @@ -40,11 +59,15 @@ var Settings = { 'keys should be a string or array of strings' ); - var sid = subscriptions.length; + const sid = subscriptions.length; subscriptions.push({keys: keys, callback: callback}); return sid; }, + /** + * + * @param {*} watchId a number identifying which set of monitoried keys is to be cleared + */ clearWatch(watchId: number) { if (watchId < subscriptions.length) { subscriptions[watchId] = {keys: [], callback: null}; @@ -53,8 +76,8 @@ var Settings = { _sendObservations(body: Object) { Object.keys(body).forEach((key) => { - var newValue = body[key]; - var didChange = this._settings[key] !== newValue; + const newValue = body[key]; + const didChange = this._settings[key] !== newValue; this._settings[key] = newValue; if (didChange) { diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js new file mode 100644 index 000000000000..e9c26b0798c5 --- /dev/null +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.unmock('../Settings.android'); +jest.unmock('Platform'); + +jest.unmock('BatchedBridge'); +jest.unmock('defineLazyObjectProperty'); +jest.unmock('MessageQueue'); +jest.unmock('NativeModules'); + +let Platform = require('Platform'); + +const Settings = require('Settings'); + +describe('Settings', () => { + + describe('setting and getting', () => { + it('should set values and retrieve them', () => { + + if (Platform.OS == 'ios') { + expect(true).toEqual(true); + return; + } + + Settings.set({oneKey: 1}); + Settings.set({twoKey: 2.0}); + Settings.set({threeKey: 'three'}); + + const oneValue = Settings.get('oneKey'); + const twoValue = Settings.get('twoKey'); + const threeValue = Settings.get('threeKey'); + + expect(oneValue === 1).toEqual(true); + expect(twoValue === 2.0).toEqual(true); + expect(threeValue === 'three').toEqual(true); + }); + }); +}); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java new file mode 100644 index 000000000000..cc202ace9e90 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.settings; + +import android.content.Context; +import android.content.SharedPreferences; +//import android.support.annotation.NonNull; +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +@ReactModule(name = SettingsModule.NAME) +public class SettingsModule extends ReactContextBaseJavaModule implements SharedPreferences.OnSharedPreferenceChangeListener{ + + @SuppressWarnings("WeakerAccess") + static final String NAME = "SettingsManager"; + + static private @NonNull String sFilename = ""; + private Boolean ignoringUpdates = false; + private SharedPreferences mPreferences; + + public SettingsModule(ReactApplicationContext context) { + super(context); + } + + @Override + public String getName() { + return SettingsModule.NAME; + } + + private SharedPreferences getPreferences() { + if (mPreferences == null) { + if (SettingsModule.sFilename.length() > 0) { + mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); + } else if (getCurrentActivity() != null) { + mPreferences = getCurrentActivity().getPreferences(Context.MODE_PRIVATE); + } + mPreferences.registerOnSharedPreferenceChangeListener(this); + } + + return mPreferences; + } + + static public void setFilename(@NonNull String filename) { + sFilename = filename; + } + + @Override + public void onCatalystInstanceDestroy() { + getPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @ReactMethod + public void setValues(ReadableMap map) { + ReadableNativeMap nativeMap = (ReadableNativeMap) map; + + this.ignoringUpdates = true; + this.setValuesToPreference(nativeMap.toHashMap(), getPreferences()); + this.ignoringUpdates = false; + } + + @SuppressWarnings("unchecked") + public Map getConstants() { + Map map; + + try { + map = (Map) getPreferences().getAll(); + + for (String key : map.keySet()) { + Object value = map.get(key); + if (value instanceof HashSet) { + ArrayList list = new ArrayList<>((HashSet) value); + map.put(key, list); + } + + } + } catch (ClassCastException ex) { + map = new HashMap<>(); + } + + HashMap constants = new HashMap<>(); + + constants.put("settings", map); + return constants; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefKey) { + if (ignoringUpdates) { + return; + } + Map prefsMap = getPreferences().getAll(); + + WritableMap map = Arguments.createMap(); + + for (String key: prefsMap.keySet()) { + Object value = prefsMap.get(key); + + if (value == null) { + map.putNull(key); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else if (value instanceof Number) { + if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else { + map.putDouble(key, ((Number) value).doubleValue()); + } + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else { + throw new IllegalArgumentException("Could not convert " + value.getClass()); + } + } + if (getReactApplicationContext().hasActiveCatalystInstance()) { + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("settingsUpdated", map); + } + } + + private void setValuesToPreference(HashMap sourceMap, SharedPreferences pref) { + SharedPreferences.Editor editor = pref.edit(); + + for (HashMap.Entry entry : sourceMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value != null && !value.getClass().isPrimitive() && !(value instanceof String)) { + continue; + } + + if (value == null) { + editor.remove(key); + } else if (value instanceof String) { + editor.putString(key, (String) sourceMap.get(key)); + } else if (value instanceof Number) { + Float valueF = ((Number) value).floatValue(); + editor.putFloat(key, valueF); + } if (value instanceof Boolean) { + editor.putBoolean(key, (Boolean) sourceMap.get(key)); + } + + } + editor.apply(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 7d076813a4e5..89155a851741 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -52,6 +52,7 @@ import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.permissions.PermissionsModule; +import com.facebook.react.modules.settings.SettingsModule; import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; @@ -211,6 +212,12 @@ public NativeModule get() { return new PermissionsModule(context); } }), + new ModuleSpec(SettingsModule.class, new Provider() { + @Override + public NativeModule get() { + return new SettingsModule(context); + } + }), new ModuleSpec(ShareModule.class, new Provider() { @Override public NativeModule get() { From 2ddc9e61ff456aa42e1272ba0335e5081d3b4385 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Thu, 23 Mar 2017 14:38:16 -0700 Subject: [PATCH 02/16] Add settings module for android --- Libraries/Settings/Settings.android.js | 34 ---- .../Settings/{Settings.ios.js => Settings.js} | 41 ++++- Libraries/Settings/__tests__/Settings-test.js | 46 +++++ .../modules/settings/SettingsModule.java | 166 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 7 + 5 files changed, 251 insertions(+), 43 deletions(-) delete mode 100644 Libraries/Settings/Settings.android.js rename Libraries/Settings/{Settings.ios.js => Settings.js} (55%) create mode 100644 Libraries/Settings/__tests__/Settings-test.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java diff --git a/Libraries/Settings/Settings.android.js b/Libraries/Settings/Settings.android.js deleted file mode 100644 index d13a32ba9218..000000000000 --- a/Libraries/Settings/Settings.android.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule Settings - * @flow - */ -'use strict'; - -var Settings = { - get(key: string): mixed { - console.warn('Settings is not yet supported on Android'); - return null; - }, - - set(settings: Object) { - console.warn('Settings is not yet supported on Android'); - }, - - watchKeys(keys: string | Array, callback: Function): number { - console.warn('Settings is not yet supported on Android'); - return -1; - }, - - clearWatch(watchId: number) { - console.warn('Settings is not yet supported on Android'); - }, -}; - -module.exports = Settings; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.js similarity index 55% rename from Libraries/Settings/Settings.ios.js rename to Libraries/Settings/Settings.js index beda544b43e5..5752322c7777 100644 --- a/Libraries/Settings/Settings.ios.js +++ b/Libraries/Settings/Settings.js @@ -11,25 +11,44 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTSettingsManager = require('NativeModules').SettingsManager; +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const RCTSettingsManager = require('NativeModules').SettingsManager; -var invariant = require('fbjs/lib/invariant'); +const invariant = require('fbjs/lib/invariant'); -var subscriptions: Array<{keys: Array, callback: ?Function}> = []; +const subscriptions: Array<{keys: Array; callback: ?Function}> = []; -var Settings = { +/** + * @class + * @description + * An interface to the native "preferences" file storage that uses simple key-value pairs + * + */ +const Settings = { _settings: RCTSettingsManager && RCTSettingsManager.settings, get(key: string): mixed { return this._settings[key]; }, - set(settings: Object) { + /** + * Sets the value for a name-value pairs + * @param settings the object with the name/value pairs to set + * + * Note that on android the only allowed value types are number, string and boolean + */ + set(settings: Object): void { this._settings = Object.assign(this._settings, settings); RCTSettingsManager.setValues(settings); }, + + /** + * Monitor one or more keys for changed values + * @param keys a string or an array of strings naming the keys to monitor + * @callback the callback to invoke when one of the specified keys updates its value + * @returns a number representing the watch ID which can be used to clear the watch + */ watchKeys(keys: string | Array, callback: Function): number { if (typeof keys === 'string') { keys = [keys]; @@ -40,11 +59,15 @@ var Settings = { 'keys should be a string or array of strings' ); - var sid = subscriptions.length; + const sid = subscriptions.length; subscriptions.push({keys: keys, callback: callback}); return sid; }, + /** + * + * @param {*} watchId a number identifying which set of monitoried keys is to be cleared + */ clearWatch(watchId: number) { if (watchId < subscriptions.length) { subscriptions[watchId] = {keys: [], callback: null}; @@ -53,8 +76,8 @@ var Settings = { _sendObservations(body: Object) { Object.keys(body).forEach((key) => { - var newValue = body[key]; - var didChange = this._settings[key] !== newValue; + const newValue = body[key]; + const didChange = this._settings[key] !== newValue; this._settings[key] = newValue; if (didChange) { diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js new file mode 100644 index 000000000000..e9c26b0798c5 --- /dev/null +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.unmock('../Settings.android'); +jest.unmock('Platform'); + +jest.unmock('BatchedBridge'); +jest.unmock('defineLazyObjectProperty'); +jest.unmock('MessageQueue'); +jest.unmock('NativeModules'); + +let Platform = require('Platform'); + +const Settings = require('Settings'); + +describe('Settings', () => { + + describe('setting and getting', () => { + it('should set values and retrieve them', () => { + + if (Platform.OS == 'ios') { + expect(true).toEqual(true); + return; + } + + Settings.set({oneKey: 1}); + Settings.set({twoKey: 2.0}); + Settings.set({threeKey: 'three'}); + + const oneValue = Settings.get('oneKey'); + const twoValue = Settings.get('twoKey'); + const threeValue = Settings.get('threeKey'); + + expect(oneValue === 1).toEqual(true); + expect(twoValue === 2.0).toEqual(true); + expect(threeValue === 'three').toEqual(true); + }); + }); +}); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java new file mode 100644 index 000000000000..cc202ace9e90 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.settings; + +import android.content.Context; +import android.content.SharedPreferences; +//import android.support.annotation.NonNull; +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +@ReactModule(name = SettingsModule.NAME) +public class SettingsModule extends ReactContextBaseJavaModule implements SharedPreferences.OnSharedPreferenceChangeListener{ + + @SuppressWarnings("WeakerAccess") + static final String NAME = "SettingsManager"; + + static private @NonNull String sFilename = ""; + private Boolean ignoringUpdates = false; + private SharedPreferences mPreferences; + + public SettingsModule(ReactApplicationContext context) { + super(context); + } + + @Override + public String getName() { + return SettingsModule.NAME; + } + + private SharedPreferences getPreferences() { + if (mPreferences == null) { + if (SettingsModule.sFilename.length() > 0) { + mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); + } else if (getCurrentActivity() != null) { + mPreferences = getCurrentActivity().getPreferences(Context.MODE_PRIVATE); + } + mPreferences.registerOnSharedPreferenceChangeListener(this); + } + + return mPreferences; + } + + static public void setFilename(@NonNull String filename) { + sFilename = filename; + } + + @Override + public void onCatalystInstanceDestroy() { + getPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @ReactMethod + public void setValues(ReadableMap map) { + ReadableNativeMap nativeMap = (ReadableNativeMap) map; + + this.ignoringUpdates = true; + this.setValuesToPreference(nativeMap.toHashMap(), getPreferences()); + this.ignoringUpdates = false; + } + + @SuppressWarnings("unchecked") + public Map getConstants() { + Map map; + + try { + map = (Map) getPreferences().getAll(); + + for (String key : map.keySet()) { + Object value = map.get(key); + if (value instanceof HashSet) { + ArrayList list = new ArrayList<>((HashSet) value); + map.put(key, list); + } + + } + } catch (ClassCastException ex) { + map = new HashMap<>(); + } + + HashMap constants = new HashMap<>(); + + constants.put("settings", map); + return constants; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefKey) { + if (ignoringUpdates) { + return; + } + Map prefsMap = getPreferences().getAll(); + + WritableMap map = Arguments.createMap(); + + for (String key: prefsMap.keySet()) { + Object value = prefsMap.get(key); + + if (value == null) { + map.putNull(key); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else if (value instanceof Number) { + if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else { + map.putDouble(key, ((Number) value).doubleValue()); + } + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else { + throw new IllegalArgumentException("Could not convert " + value.getClass()); + } + } + if (getReactApplicationContext().hasActiveCatalystInstance()) { + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("settingsUpdated", map); + } + } + + private void setValuesToPreference(HashMap sourceMap, SharedPreferences pref) { + SharedPreferences.Editor editor = pref.edit(); + + for (HashMap.Entry entry : sourceMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value != null && !value.getClass().isPrimitive() && !(value instanceof String)) { + continue; + } + + if (value == null) { + editor.remove(key); + } else if (value instanceof String) { + editor.putString(key, (String) sourceMap.get(key)); + } else if (value instanceof Number) { + Float valueF = ((Number) value).floatValue(); + editor.putFloat(key, valueF); + } if (value instanceof Boolean) { + editor.putBoolean(key, (Boolean) sourceMap.get(key)); + } + + } + editor.apply(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 7d076813a4e5..89155a851741 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -52,6 +52,7 @@ import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.permissions.PermissionsModule; +import com.facebook.react.modules.settings.SettingsModule; import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; @@ -211,6 +212,12 @@ public NativeModule get() { return new PermissionsModule(context); } }), + new ModuleSpec(SettingsModule.class, new Provider() { + @Override + public NativeModule get() { + return new SettingsModule(context); + } + }), new ModuleSpec(ShareModule.class, new Provider() { @Override public NativeModule get() { From 11ef48f3e9bfb2b80d4cf9f925ffb0355009ce3a Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Sat, 25 Mar 2017 14:53:13 -0700 Subject: [PATCH 03/16] Fix Settings test --- Libraries/Settings/__tests__/Settings-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js index e9c26b0798c5..47da4cb0356d 100644 --- a/Libraries/Settings/__tests__/Settings-test.js +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -8,7 +8,7 @@ */ 'use strict'; -jest.unmock('../Settings.android'); +jest.unmock('../Settings'); jest.unmock('Platform'); jest.unmock('BatchedBridge'); From 63d4d55170db064663e77c16332bd1d3b80a1d8d Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 09:06:59 -0700 Subject: [PATCH 04/16] Add buck files for settings module --- .../com/facebook/react/modules/settings/BUCK | 16 ++++++++++++++++ .../src/main/java/com/facebook/react/shell/BUCK | 1 + 2 files changed, 17 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK new file mode 100644 index 000000000000..0b2d2287a415 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK @@ -0,0 +1,16 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "settings", + srcs = glob(["*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("third-party/java/infer-annotations:infer-annotations"), + react_native_dep("third-party/java/jsr-305:jsr-305"), + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/module/annotations:annotations"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index a928e6688d1b..1acf0fea0911 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -36,6 +36,7 @@ android_library( react_native_target("java/com/facebook/react/modules/network:network"), react_native_target("java/com/facebook/react/modules/permissions:permissions"), react_native_target("java/com/facebook/react/modules/share:share"), + react_native_target("java/com/facebook/react/modules/settings:settings"), react_native_target("java/com/facebook/react/modules/statusbar:statusbar"), react_native_target("java/com/facebook/react/modules/storage:storage"), react_native_target("java/com/facebook/react/modules/timepicker:timepicker"), From 5b969054d0cf6ff9f5d801e27da7d05f0dbb6400 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 10:27:44 -0700 Subject: [PATCH 05/16] fix settings buck file --- .../src/main/java/com/facebook/react/modules/settings/BUCK | 4 +++- .../com/facebook/react/modules/settings/SettingsModule.java | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK index 0b2d2287a415..61cc1246293d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK @@ -2,7 +2,7 @@ include_defs("//ReactAndroid/DEFS") android_library( name = "settings", - srcs = glob(["*.java"]), + srcs = glob(["**/*.java"]), visibility = [ "PUBLIC", ], @@ -12,5 +12,7 @@ android_library( react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/module/annotations:annotations"), + react_native_target("java/com/facebook/react/modules/core:core"), ], ) + diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index cc202ace9e90..4c5dc8fbd12e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -11,8 +11,6 @@ import android.content.Context; import android.content.SharedPreferences; -//import android.support.annotation.NonNull; -import android.support.annotation.NonNull; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -35,7 +33,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Share @SuppressWarnings("WeakerAccess") static final String NAME = "SettingsManager"; - static private @NonNull String sFilename = ""; + static private String sFilename = ""; private Boolean ignoringUpdates = false; private SharedPreferences mPreferences; @@ -61,7 +59,7 @@ private SharedPreferences getPreferences() { return mPreferences; } - static public void setFilename(@NonNull String filename) { + static public void setFilename(String filename) { sFilename = filename; } From 57ca0d05faec3782e051dfa129262d9801db2867 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 11:24:23 -0700 Subject: [PATCH 06/16] Add a default settings file name --- .../com/facebook/react/modules/settings/SettingsModule.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index 4c5dc8fbd12e..a6f63f786ab7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -32,6 +32,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Share @SuppressWarnings("WeakerAccess") static final String NAME = "SettingsManager"; + static final String DEFAULT_FILE_NAME = "ReactNative"; static private String sFilename = ""; private Boolean ignoringUpdates = false; @@ -52,6 +53,8 @@ private SharedPreferences getPreferences() { mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); } else if (getCurrentActivity() != null) { mPreferences = getCurrentActivity().getPreferences(Context.MODE_PRIVATE); + } else { + mPreferences = getReactApplicationContext().getSharedPreferences(DEFAULT_FILE_NAME, Context.MODE_PRIVATE); } mPreferences.registerOnSharedPreferenceChangeListener(this); } From bd5b23d7b4750ee20990c38a79a061f20292cfd8 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Thu, 23 Mar 2017 14:38:16 -0700 Subject: [PATCH 07/16] Add settings module for android --- Libraries/Settings/Settings.android.js | 34 ---- .../Settings/{Settings.ios.js => Settings.js} | 41 ++++- Libraries/Settings/__tests__/Settings-test.js | 46 +++++ .../modules/settings/SettingsModule.java | 166 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 7 + 5 files changed, 251 insertions(+), 43 deletions(-) delete mode 100644 Libraries/Settings/Settings.android.js rename Libraries/Settings/{Settings.ios.js => Settings.js} (55%) create mode 100644 Libraries/Settings/__tests__/Settings-test.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java diff --git a/Libraries/Settings/Settings.android.js b/Libraries/Settings/Settings.android.js deleted file mode 100644 index d13a32ba9218..000000000000 --- a/Libraries/Settings/Settings.android.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule Settings - * @flow - */ -'use strict'; - -var Settings = { - get(key: string): mixed { - console.warn('Settings is not yet supported on Android'); - return null; - }, - - set(settings: Object) { - console.warn('Settings is not yet supported on Android'); - }, - - watchKeys(keys: string | Array, callback: Function): number { - console.warn('Settings is not yet supported on Android'); - return -1; - }, - - clearWatch(watchId: number) { - console.warn('Settings is not yet supported on Android'); - }, -}; - -module.exports = Settings; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.js similarity index 55% rename from Libraries/Settings/Settings.ios.js rename to Libraries/Settings/Settings.js index beda544b43e5..5752322c7777 100644 --- a/Libraries/Settings/Settings.ios.js +++ b/Libraries/Settings/Settings.js @@ -11,25 +11,44 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTSettingsManager = require('NativeModules').SettingsManager; +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const RCTSettingsManager = require('NativeModules').SettingsManager; -var invariant = require('fbjs/lib/invariant'); +const invariant = require('fbjs/lib/invariant'); -var subscriptions: Array<{keys: Array, callback: ?Function}> = []; +const subscriptions: Array<{keys: Array; callback: ?Function}> = []; -var Settings = { +/** + * @class + * @description + * An interface to the native "preferences" file storage that uses simple key-value pairs + * + */ +const Settings = { _settings: RCTSettingsManager && RCTSettingsManager.settings, get(key: string): mixed { return this._settings[key]; }, - set(settings: Object) { + /** + * Sets the value for a name-value pairs + * @param settings the object with the name/value pairs to set + * + * Note that on android the only allowed value types are number, string and boolean + */ + set(settings: Object): void { this._settings = Object.assign(this._settings, settings); RCTSettingsManager.setValues(settings); }, + + /** + * Monitor one or more keys for changed values + * @param keys a string or an array of strings naming the keys to monitor + * @callback the callback to invoke when one of the specified keys updates its value + * @returns a number representing the watch ID which can be used to clear the watch + */ watchKeys(keys: string | Array, callback: Function): number { if (typeof keys === 'string') { keys = [keys]; @@ -40,11 +59,15 @@ var Settings = { 'keys should be a string or array of strings' ); - var sid = subscriptions.length; + const sid = subscriptions.length; subscriptions.push({keys: keys, callback: callback}); return sid; }, + /** + * + * @param {*} watchId a number identifying which set of monitoried keys is to be cleared + */ clearWatch(watchId: number) { if (watchId < subscriptions.length) { subscriptions[watchId] = {keys: [], callback: null}; @@ -53,8 +76,8 @@ var Settings = { _sendObservations(body: Object) { Object.keys(body).forEach((key) => { - var newValue = body[key]; - var didChange = this._settings[key] !== newValue; + const newValue = body[key]; + const didChange = this._settings[key] !== newValue; this._settings[key] = newValue; if (didChange) { diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js new file mode 100644 index 000000000000..e9c26b0798c5 --- /dev/null +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.unmock('../Settings.android'); +jest.unmock('Platform'); + +jest.unmock('BatchedBridge'); +jest.unmock('defineLazyObjectProperty'); +jest.unmock('MessageQueue'); +jest.unmock('NativeModules'); + +let Platform = require('Platform'); + +const Settings = require('Settings'); + +describe('Settings', () => { + + describe('setting and getting', () => { + it('should set values and retrieve them', () => { + + if (Platform.OS == 'ios') { + expect(true).toEqual(true); + return; + } + + Settings.set({oneKey: 1}); + Settings.set({twoKey: 2.0}); + Settings.set({threeKey: 'three'}); + + const oneValue = Settings.get('oneKey'); + const twoValue = Settings.get('twoKey'); + const threeValue = Settings.get('threeKey'); + + expect(oneValue === 1).toEqual(true); + expect(twoValue === 2.0).toEqual(true); + expect(threeValue === 'three').toEqual(true); + }); + }); +}); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java new file mode 100644 index 000000000000..cc202ace9e90 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.settings; + +import android.content.Context; +import android.content.SharedPreferences; +//import android.support.annotation.NonNull; +import android.support.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +@ReactModule(name = SettingsModule.NAME) +public class SettingsModule extends ReactContextBaseJavaModule implements SharedPreferences.OnSharedPreferenceChangeListener{ + + @SuppressWarnings("WeakerAccess") + static final String NAME = "SettingsManager"; + + static private @NonNull String sFilename = ""; + private Boolean ignoringUpdates = false; + private SharedPreferences mPreferences; + + public SettingsModule(ReactApplicationContext context) { + super(context); + } + + @Override + public String getName() { + return SettingsModule.NAME; + } + + private SharedPreferences getPreferences() { + if (mPreferences == null) { + if (SettingsModule.sFilename.length() > 0) { + mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); + } else if (getCurrentActivity() != null) { + mPreferences = getCurrentActivity().getPreferences(Context.MODE_PRIVATE); + } + mPreferences.registerOnSharedPreferenceChangeListener(this); + } + + return mPreferences; + } + + static public void setFilename(@NonNull String filename) { + sFilename = filename; + } + + @Override + public void onCatalystInstanceDestroy() { + getPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @ReactMethod + public void setValues(ReadableMap map) { + ReadableNativeMap nativeMap = (ReadableNativeMap) map; + + this.ignoringUpdates = true; + this.setValuesToPreference(nativeMap.toHashMap(), getPreferences()); + this.ignoringUpdates = false; + } + + @SuppressWarnings("unchecked") + public Map getConstants() { + Map map; + + try { + map = (Map) getPreferences().getAll(); + + for (String key : map.keySet()) { + Object value = map.get(key); + if (value instanceof HashSet) { + ArrayList list = new ArrayList<>((HashSet) value); + map.put(key, list); + } + + } + } catch (ClassCastException ex) { + map = new HashMap<>(); + } + + HashMap constants = new HashMap<>(); + + constants.put("settings", map); + return constants; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefKey) { + if (ignoringUpdates) { + return; + } + Map prefsMap = getPreferences().getAll(); + + WritableMap map = Arguments.createMap(); + + for (String key: prefsMap.keySet()) { + Object value = prefsMap.get(key); + + if (value == null) { + map.putNull(key); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else if (value instanceof Number) { + if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else { + map.putDouble(key, ((Number) value).doubleValue()); + } + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else { + throw new IllegalArgumentException("Could not convert " + value.getClass()); + } + } + if (getReactApplicationContext().hasActiveCatalystInstance()) { + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("settingsUpdated", map); + } + } + + private void setValuesToPreference(HashMap sourceMap, SharedPreferences pref) { + SharedPreferences.Editor editor = pref.edit(); + + for (HashMap.Entry entry : sourceMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value != null && !value.getClass().isPrimitive() && !(value instanceof String)) { + continue; + } + + if (value == null) { + editor.remove(key); + } else if (value instanceof String) { + editor.putString(key, (String) sourceMap.get(key)); + } else if (value instanceof Number) { + Float valueF = ((Number) value).floatValue(); + editor.putFloat(key, valueF); + } if (value instanceof Boolean) { + editor.putBoolean(key, (Boolean) sourceMap.get(key)); + } + + } + editor.apply(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 7d076813a4e5..89155a851741 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -52,6 +52,7 @@ import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.permissions.PermissionsModule; +import com.facebook.react.modules.settings.SettingsModule; import com.facebook.react.modules.share.ShareModule; import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; @@ -211,6 +212,12 @@ public NativeModule get() { return new PermissionsModule(context); } }), + new ModuleSpec(SettingsModule.class, new Provider() { + @Override + public NativeModule get() { + return new SettingsModule(context); + } + }), new ModuleSpec(ShareModule.class, new Provider() { @Override public NativeModule get() { From 0fef2c34d9b12821d176a22e719f6561512c5bd9 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Sat, 25 Mar 2017 14:53:13 -0700 Subject: [PATCH 08/16] Fix Settings test --- Libraries/Settings/__tests__/Settings-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js index e9c26b0798c5..47da4cb0356d 100644 --- a/Libraries/Settings/__tests__/Settings-test.js +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -8,7 +8,7 @@ */ 'use strict'; -jest.unmock('../Settings.android'); +jest.unmock('../Settings'); jest.unmock('Platform'); jest.unmock('BatchedBridge'); From dbb65a645aa7e2650ea4084b9c3fdd4ce4319fd9 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 09:06:59 -0700 Subject: [PATCH 09/16] Add buck files for settings module --- .../com/facebook/react/modules/settings/BUCK | 16 ++++++++++++++++ .../src/main/java/com/facebook/react/shell/BUCK | 1 + 2 files changed, 17 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK new file mode 100644 index 000000000000..0b2d2287a415 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK @@ -0,0 +1,16 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "settings", + srcs = glob(["*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("third-party/java/infer-annotations:infer-annotations"), + react_native_dep("third-party/java/jsr-305:jsr-305"), + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/module/annotations:annotations"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index a928e6688d1b..1acf0fea0911 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -36,6 +36,7 @@ android_library( react_native_target("java/com/facebook/react/modules/network:network"), react_native_target("java/com/facebook/react/modules/permissions:permissions"), react_native_target("java/com/facebook/react/modules/share:share"), + react_native_target("java/com/facebook/react/modules/settings:settings"), react_native_target("java/com/facebook/react/modules/statusbar:statusbar"), react_native_target("java/com/facebook/react/modules/storage:storage"), react_native_target("java/com/facebook/react/modules/timepicker:timepicker"), From 9b2a396f32f985d601c1c6a0da334f85f0d96323 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 10:27:44 -0700 Subject: [PATCH 10/16] fix settings buck file --- .../src/main/java/com/facebook/react/modules/settings/BUCK | 4 +++- .../com/facebook/react/modules/settings/SettingsModule.java | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK index 0b2d2287a415..61cc1246293d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/BUCK @@ -2,7 +2,7 @@ include_defs("//ReactAndroid/DEFS") android_library( name = "settings", - srcs = glob(["*.java"]), + srcs = glob(["**/*.java"]), visibility = [ "PUBLIC", ], @@ -12,5 +12,7 @@ android_library( react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/module/annotations:annotations"), + react_native_target("java/com/facebook/react/modules/core:core"), ], ) + diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index cc202ace9e90..4c5dc8fbd12e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -11,8 +11,6 @@ import android.content.Context; import android.content.SharedPreferences; -//import android.support.annotation.NonNull; -import android.support.annotation.NonNull; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -35,7 +33,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Share @SuppressWarnings("WeakerAccess") static final String NAME = "SettingsManager"; - static private @NonNull String sFilename = ""; + static private String sFilename = ""; private Boolean ignoringUpdates = false; private SharedPreferences mPreferences; @@ -61,7 +59,7 @@ private SharedPreferences getPreferences() { return mPreferences; } - static public void setFilename(@NonNull String filename) { + static public void setFilename(String filename) { sFilename = filename; } From 1aee785295a116dcb961fda9e59f318a32da5fde Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Mon, 27 Mar 2017 11:24:23 -0700 Subject: [PATCH 11/16] Add a default settings file name --- .../com/facebook/react/modules/settings/SettingsModule.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index 4c5dc8fbd12e..a6f63f786ab7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -32,6 +32,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Share @SuppressWarnings("WeakerAccess") static final String NAME = "SettingsManager"; + static final String DEFAULT_FILE_NAME = "ReactNative"; static private String sFilename = ""; private Boolean ignoringUpdates = false; @@ -52,6 +53,8 @@ private SharedPreferences getPreferences() { mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); } else if (getCurrentActivity() != null) { mPreferences = getCurrentActivity().getPreferences(Context.MODE_PRIVATE); + } else { + mPreferences = getReactApplicationContext().getSharedPreferences(DEFAULT_FILE_NAME, Context.MODE_PRIVATE); } mPreferences.registerOnSharedPreferenceChangeListener(this); } From 1214f84899ad7f09f17def2f6633bfccc548b3cc Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Tue, 4 Apr 2017 10:29:22 -0700 Subject: [PATCH 12/16] Clarify tests. Make value conversion more abstract. Fix constants export from Preferences. --- Libraries/Settings/Settings.js | 2 +- Libraries/Settings/__tests__/Settings-test.js | 6 +- .../modules/settings/SettingsModule.java | 90 +++++++++---------- 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/Libraries/Settings/Settings.js b/Libraries/Settings/Settings.js index 5752322c7777..78390ad11915 100644 --- a/Libraries/Settings/Settings.js +++ b/Libraries/Settings/Settings.js @@ -38,7 +38,7 @@ const Settings = { * Note that on android the only allowed value types are number, string and boolean */ set(settings: Object): void { - this._settings = Object.assign(this._settings, settings); + Object.assign(this._settings, settings); RCTSettingsManager.setValues(settings); }, diff --git a/Libraries/Settings/__tests__/Settings-test.js b/Libraries/Settings/__tests__/Settings-test.js index 47da4cb0356d..ebc883542bde 100644 --- a/Libraries/Settings/__tests__/Settings-test.js +++ b/Libraries/Settings/__tests__/Settings-test.js @@ -38,9 +38,9 @@ describe('Settings', () => { const twoValue = Settings.get('twoKey'); const threeValue = Settings.get('threeKey'); - expect(oneValue === 1).toEqual(true); - expect(twoValue === 2.0).toEqual(true); - expect(threeValue === 'three').toEqual(true); + expect(oneValue === 1).toBe(true); + expect(twoValue === 2.0).toBe(true); + expect(threeValue === 'three').toBe(true); }); }); }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index a6f63f786ab7..2488434813aa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -17,14 +17,13 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; -import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; @ReactModule(name = SettingsModule.NAME) @@ -72,36 +71,57 @@ public void onCatalystInstanceDestroy() { } @ReactMethod - public void setValues(ReadableMap map) { - ReadableNativeMap nativeMap = (ReadableNativeMap) map; - + public void setValues(ReadableMap values) { this.ignoringUpdates = true; - this.setValuesToPreference(nativeMap.toHashMap(), getPreferences()); + + ReadableMapKeySetIterator iterator = values.keySetIterator(); + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = values.getType(key); + + switch (type) { + case Null: + mPreferences.edit().remove(key).apply(); + break; + + case Boolean: + mPreferences.edit().putBoolean(key, values.getBoolean(key)).apply(); + break; + + case Number: + mPreferences.edit().putFloat(key, (float) values.getDouble(key)).apply(); + break; + + case String: + mPreferences.edit().putString(key, values.getString(key)).apply(); + break; + + case Map: // Not supported + throw new IllegalArgumentException("Could not store Map in Settings"); + + case Array: // Not supported + throw new IllegalArgumentException("Could not store Array in Settings"); + } + } + this.ignoringUpdates = false; } - @SuppressWarnings("unchecked") public Map getConstants() { - Map map; - - try { - map = (Map) getPreferences().getAll(); - - for (String key : map.keySet()) { - Object value = map.get(key); - if (value instanceof HashSet) { - ArrayList list = new ArrayList<>((HashSet) value); - map.put(key, list); - } + Map exportedValuePairs = new HashMap(); + Map prefsMap = getPreferences().getAll(); + for (String key : prefsMap.keySet()) { + Object value = prefsMap.get(key); + if (value instanceof String || value.getClass().isPrimitive()) { + exportedValuePairs.put(key, value); } - } catch (ClassCastException ex) { - map = new HashMap<>(); } HashMap constants = new HashMap<>(); - constants.put("settings", map); + constants.put("settings", exportedValuePairs); return constants; } @@ -138,30 +158,4 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin .emit("settingsUpdated", map); } } - - private void setValuesToPreference(HashMap sourceMap, SharedPreferences pref) { - SharedPreferences.Editor editor = pref.edit(); - - for (HashMap.Entry entry : sourceMap.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - if (value != null && !value.getClass().isPrimitive() && !(value instanceof String)) { - continue; - } - - if (value == null) { - editor.remove(key); - } else if (value instanceof String) { - editor.putString(key, (String) sourceMap.get(key)); - } else if (value instanceof Number) { - Float valueF = ((Number) value).floatValue(); - editor.putFloat(key, valueF); - } if (value instanceof Boolean) { - editor.putBoolean(key, (Boolean) sourceMap.get(key)); - } - - } - editor.apply(); - } } From 4f11f673730578997b44014db96e330a110b06d8 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Wed, 5 Apr 2017 09:12:11 -0700 Subject: [PATCH 13/16] Change Settings.ios.js to Settings.js in docsList.js --- website/server/docsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/docsList.js b/website/server/docsList.js index 47bad1f8209f..e9be393ffe80 100644 --- a/website/server/docsList.js +++ b/website/server/docsList.js @@ -81,7 +81,7 @@ const apis = [ '../Libraries/PermissionsAndroid/PermissionsAndroid.js', '../Libraries/Utilities/PixelRatio.js', '../Libraries/PushNotificationIOS/PushNotificationIOS.js', - '../Libraries/Settings/Settings.ios.js', + '../Libraries/Settings/Settings.js', '../Libraries/Share/Share.js', '../Libraries/Components/StatusBar/StatusBarIOS.ios.js', '../Libraries/StyleSheet/StyleSheet.js', From d494eaaf1e7073a3bfa34b39ed464d1892c39f78 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Fri, 7 Apr 2017 09:37:55 -0700 Subject: [PATCH 14/16] Rename setFilename to setSharedPreferencesFilename and javadoc-ed it. Renamed getPreferences to ensurePreferences. Improved description of some thrown exceptions. --- .../modules/settings/SettingsModule.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index 2488434813aa..5bedd8eb9756 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -27,11 +27,14 @@ import java.util.Map; @ReactModule(name = SettingsModule.NAME) -public class SettingsModule extends ReactContextBaseJavaModule implements SharedPreferences.OnSharedPreferenceChangeListener{ +public class SettingsModule extends ReactContextBaseJavaModule implements SharedPreferences.OnSharedPreferenceChangeListener{ @SuppressWarnings("WeakerAccess") static final String NAME = "SettingsManager"; static final String DEFAULT_FILE_NAME = "ReactNative"; + static final String TAG = "SettingsMmodule"; + + static boolean sSettingsModuleInstantiated = false; static private String sFilename = ""; private Boolean ignoringUpdates = false; @@ -39,6 +42,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Share public SettingsModule(ReactApplicationContext context) { super(context); + } @Override @@ -46,7 +50,7 @@ public String getName() { return SettingsModule.NAME; } - private SharedPreferences getPreferences() { + private SharedPreferences ensurePreferences() { if (mPreferences == null) { if (SettingsModule.sFilename.length() > 0) { mPreferences = getReactApplicationContext().getSharedPreferences(SettingsModule.sFilename, Context.MODE_PRIVATE); @@ -55,19 +59,33 @@ private SharedPreferences getPreferences() { } else { mPreferences = getReactApplicationContext().getSharedPreferences(DEFAULT_FILE_NAME, Context.MODE_PRIVATE); } + sSettingsModuleInstantiated = true; mPreferences.registerOnSharedPreferenceChangeListener(this); } return mPreferences; } - static public void setFilename(String filename) { + /*** + * Configure the Settings Module to use a SharedPreferences file with a specific name as its backing store. + * Useful if the Settings Module needs to use an existing SharedPreferences file or to ensure that Settings Modules + * belonging to different ReactPackages will all share the same SharedPreferences file for their backing store. + *

+ * Note:this method may only be called before a Settings Module initialization (i.e. before a ReactMainPackage is instantiated) + * An app would typically invoke this function in the app's main activity before setting up the React Native environment + * @param filename the name of the SharedPreferences file to use or create for the backing store + * @throws IllegalStateException if called after instantiation of a Settings module + */ + static public void setSharedPreferencesFilename(String filename) { + if (sSettingsModuleInstantiated) { + throw new IllegalStateException("Attempt to set Settings Module SharedPreferences file name after a Settings Module instantiation"); + } sFilename = filename; } @Override public void onCatalystInstanceDestroy() { - getPreferences().unregisterOnSharedPreferenceChangeListener(this); + ensurePreferences().unregisterOnSharedPreferenceChangeListener(this); } @ReactMethod @@ -98,10 +116,10 @@ public void setValues(ReadableMap values) { break; case Map: // Not supported - throw new IllegalArgumentException("Could not store Map in Settings"); + throw new IllegalArgumentException(TAG + ": Cannot not store Map as value in Settings"); case Array: // Not supported - throw new IllegalArgumentException("Could not store Array in Settings"); + throw new IllegalArgumentException(TAG + ": Cannot not store Array as value in Settings"); } } @@ -110,7 +128,7 @@ public void setValues(ReadableMap values) { public Map getConstants() { Map exportedValuePairs = new HashMap(); - Map prefsMap = getPreferences().getAll(); + Map prefsMap = ensurePreferences().getAll(); for (String key : prefsMap.keySet()) { Object value = prefsMap.get(key); @@ -130,7 +148,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin if (ignoringUpdates) { return; } - Map prefsMap = getPreferences().getAll(); + Map prefsMap = ensurePreferences().getAll(); WritableMap map = Arguments.createMap(); From 5c8d6162befabcf51dec7861bd1f73c4fbdb1ace Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Sat, 8 Apr 2017 12:47:51 -0700 Subject: [PATCH 15/16] Make switch statement more efficient. --- .../react/modules/settings/SettingsModule.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index 5bedd8eb9756..eb56d9d1a2a9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -93,35 +93,32 @@ public void setValues(ReadableMap values) { this.ignoringUpdates = true; ReadableMapKeySetIterator iterator = values.keySetIterator(); + SharedPreferences.Editor editor = mPreferences.edit(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = values.getType(key); - + switch (type) { case Null: - mPreferences.edit().remove(key).apply(); + editor.remove(key); break; - case Boolean: - mPreferences.edit().putBoolean(key, values.getBoolean(key)).apply(); + editor.putBoolean(key, values.getBoolean(key)); break; - case Number: - mPreferences.edit().putFloat(key, (float) values.getDouble(key)).apply(); + editor.putFloat(key, (float) values.getDouble(key)); break; - case String: - mPreferences.edit().putString(key, values.getString(key)).apply(); + editor.putString(key, values.getString(key)); break; - case Map: // Not supported throw new IllegalArgumentException(TAG + ": Cannot not store Map as value in Settings"); - case Array: // Not supported throw new IllegalArgumentException(TAG + ": Cannot not store Array as value in Settings"); } } + editor.apply(); this.ignoringUpdates = false; } From 94bd0323f282b61bfd52dfa90377e045e7f61982 Mon Sep 17 00:00:00 2001 From: Greg Herlihy Date: Sat, 8 Apr 2017 13:53:57 -0700 Subject: [PATCH 16/16] Removed thrown exception when unexportable type is encountered.. Logged when unexportable types are encountered. --- .../react/modules/settings/SettingsModule.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java index eb56d9d1a2a9..5acb66a0dcb6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/settings/SettingsModule.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -34,7 +35,7 @@ public class SettingsModule extends ReactContextBaseJavaModule implements Shared static final String DEFAULT_FILE_NAME = "ReactNative"; static final String TAG = "SettingsMmodule"; - static boolean sSettingsModuleInstantiated = false; + private static boolean sSettingsModuleInstantiated = false; static private String sFilename = ""; private Boolean ignoringUpdates = false; @@ -98,7 +99,7 @@ public void setValues(ReadableMap values) { while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = values.getType(key); - + switch (type) { case Null: editor.remove(key); @@ -131,6 +132,9 @@ public Map getConstants() { Object value = prefsMap.get(key); if (value instanceof String || value.getClass().isPrimitive()) { exportedValuePairs.put(key, value); + } else { + // StringSets are not exported - could be used in the future to store JSON encoded arrays and maps + Log.d(TAG, "unexported setting: " + key); } } @@ -165,7 +169,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin } else if (value instanceof Boolean) { map.putBoolean(key, (Boolean) value); } else { - throw new IllegalArgumentException("Could not convert " + value.getClass()); + Log.d(TAG, "ignoring setting: " + key); } } if (getReactApplicationContext().hasActiveCatalystInstance()) {