-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Add Settings Module Support for the Android Platform #13142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
fe2b7ea
7add8a6
2ddc9e6
47fc744
11ef48f
63d4d55
d5e7a55
5b96905
57ca0d0
bd5b23d
0fef2c3
dbb65a6
9b2a396
1aee785
724c18c
f776849
1214f84
c1a7ed8
4f11f67
18e3561
d494eaa
8ffa9c7
92371df
5c8d616
94bd032
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe using |
||
| expect(twoValue === 2.0).toEqual(true); | ||
| expect(threeValue === 'three').toEqual(true); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| 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"), | ||
| react_native_target("java/com/facebook/react/modules/core:core"), | ||
| ], | ||
| ) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| /* | ||
| * 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 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 final String DEFAULT_FILE_NAME = "ReactNative"; | ||
|
|
||
| static private 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with this android functionality, but I believe there is a difference in using getSharedPreferences vs getPreferences
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, and differing calls here are entirely intentional. In the section above we allow the programmer the option of storing the preferences in a file with a specific name (say, to match an existing shared preferences file already in use by the application), or, to simply store the preferences in a file with a default name.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. according to the docs,
So by default, the preferences file created by this module would be available only to the activity that created it. That may or may not be what you want, and could be changed by calling the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The settings file created by the Settings Module is accessible to that Settings module and therefore indirectly accessible to any Activity or to any other Java class that imports the Settings module. The only potential concern raised by calling Nevertheless, if separate settings files are not desired, that is, if the developer wants to ensure that every Settings Module instantiated anywhere in the the app will always use the same settings file - then that developer merely has to provide a file name to the Settings Module, and - as long as they take care that only one Settings Module is ever extant at one time - it will be the case that every Settings Module created anywhere in the app will use the same settings file which will have the provided name.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the explanation @greghe. I'm just unsure if it is possible to call
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've renamed Basically, this method exists as a convenient customization point for a developer with specialized requirements regarding handling of the Shared Preferences file - and would typically be invoked from the app's main activity just before it sets up the RN environment. Therefore invoking |
||
| } else { | ||
| mPreferences = getReactApplicationContext().getSharedPreferences(DEFAULT_FILE_NAME, Context.MODE_PRIVATE); | ||
| } | ||
| mPreferences.registerOnSharedPreferenceChangeListener(this); | ||
| } | ||
|
|
||
| return mPreferences; | ||
| } | ||
|
|
||
| static public void setFilename(String filename) { | ||
| sFilename = filename; | ||
| } | ||
|
|
||
| @Override | ||
| public void onCatalystInstanceDestroy() { | ||
| getPreferences().unregisterOnSharedPreferenceChangeListener(this); | ||
| } | ||
|
|
||
| @ReactMethod | ||
| public void setValues(ReadableMap map) { | ||
| ReadableNativeMap nativeMap = (ReadableNativeMap) map; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be great to stay at the interface level, without any casting.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how I originally implemented it and it is the cleaner implementation. I will restore it. |
||
|
|
||
| this.ignoringUpdates = true; | ||
| this.setValuesToPreference(nativeMap.toHashMap(), getPreferences()); | ||
| this.ignoringUpdates = false; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| public Map<String, Object> getConstants() { | ||
| Map<String, Object> map; | ||
|
|
||
| try { | ||
| map = (Map<String, Object>) getPreferences().getAll(); | ||
|
|
||
| for (String key : map.keySet()) { | ||
| Object value = map.get(key); | ||
| if (value instanceof HashSet) { | ||
| ArrayList<Object> list = new ArrayList<>((HashSet<Object>) value); | ||
| map.put(key, list); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this may cause trouble: the original map is being mutated - you don't want that. You will probably need an extra
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be fixed now. |
||
| } | ||
|
|
||
| } | ||
| } catch (ClassCastException ex) { | ||
| map = new HashMap<>(); | ||
| } | ||
|
|
||
| HashMap<String, Object> constants = new HashMap<>(); | ||
|
|
||
| constants.put("settings", map); | ||
| return constants; | ||
| } | ||
|
|
||
| @Override | ||
| public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefKey) { | ||
| if (ignoringUpdates) { | ||
| return; | ||
| } | ||
| Map<String, ?> 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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a little less generic message would be great, something that tells you right away what module the error relates to.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will do |
||
| } | ||
| } | ||
| if (getReactApplicationContext().hasActiveCatalystInstance()) { | ||
| getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) | ||
| .emit("settingsUpdated", map); | ||
| } | ||
| } | ||
|
|
||
| private void setValuesToPreference(HashMap<String, Object> sourceMap, SharedPreferences pref) { | ||
| SharedPreferences.Editor editor = pref.edit(); | ||
|
|
||
| for (HashMap.Entry<String, Object> 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(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just
Object.assign(this._settings, settings);would imho be more readable. Syntax isObject.assign(target, ...sources), so no need for the assignment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't actually change any code in
Settings.js(other than rename the file fromSettings.ios.js). But I agree we should probably improve this file's code as part of this change.