From 0779dd1e87f6c0c9481f4063e72aafe7805383c6 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 2 Dec 2015 10:50:09 -0800 Subject: [PATCH] Open source the Android NetInfo module Reviewed By: mkonicek Differential Revision: D2703432 fb-gh-sync-id: 4a85844f1734ec433df543c89f0fdd56fe5db13c --- Examples/UIExplorer/NetInfoExample.android.js | 158 ++++++++++++++++++ ...etInfoExample.js => NetInfoExample.ios.js} | 0 Examples/UIExplorer/UIExplorerList.android.js | 1 + Examples/UIExplorer/UIExplorerList.ios.js | 2 +- .../android/app/src/main/AndroidManifest.xml | 1 + Libraries/Network/NetInfo.js | 52 +++--- Libraries/Network/RCTNetInfo.m | 4 +- .../modules/netinfo/ConnectivityModule.java | 154 +++++++++++++++++ .../react/shell/MainReactPackage.java | 2 + 9 files changed, 347 insertions(+), 27 deletions(-) create mode 100644 Examples/UIExplorer/NetInfoExample.android.js rename Examples/UIExplorer/{NetInfoExample.js => NetInfoExample.ios.js} (100%) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/netinfo/ConnectivityModule.java diff --git a/Examples/UIExplorer/NetInfoExample.android.js b/Examples/UIExplorer/NetInfoExample.android.js new file mode 100644 index 000000000..41d772321 --- /dev/null +++ b/Examples/UIExplorer/NetInfoExample.android.js @@ -0,0 +1,158 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +const React = require('react-native'); +const { + NetInfo, // requires android.permission.ACCESS_NETWORK_STATE + Text, + View +} = React; +const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); + +const ConnectionSubscription = React.createClass({ + getInitialState() { + return { + connectionHistory: [], + }; + }, + componentDidMount: function() { + NetInfo.addEventListener( + 'change', + this._handleConnectionChange + ); + }, + componentWillUnmount: function() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionChange + ); + }, + _handleConnectionChange: function(netInfo) { + var connectionHistory = this.state.connectionHistory.slice(); + connectionHistory.push(netInfo); + this.setState({ + connectionHistory, + }); + }, + render() { + return ( + {JSON.stringify(this.state.connectionHistory)} + ); + } +}); + +const ConnectionCurrent = React.createClass({ + getInitialState() { + return { + netInfo: null, + }; + }, + componentDidMount: function() { + NetInfo.addEventListener( + 'change', + this._handleConnectionChange + ); + NetInfo.fetch().done( + (netInfo) => { this.setState({netInfo}); } + ); + }, + componentWillUnmount: function() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionChange + ); + }, + _handleConnectionChange: function(netInfo) { + this.setState({ + netInfo, + }); + }, + render() { + return ( + {JSON.stringify(this.state.netInfo)} + ); + } +}); + +const IsConnected = React.createClass({ + getInitialState() { + return { + isConnected: null, + }; + }, + componentDidMount: function() { + NetInfo.isConnected.addEventListener( + 'change', + this._handleConnectivityChange + ); + NetInfo.isConnected.fetch().done( + (isConnected) => { this.setState({isConnected}); } + ); + }, + componentWillUnmount: function() { + NetInfo.isConnected.removeEventListener( + 'change', + this._handleConnectivityChange + ); + }, + _handleConnectivityChange: function(isConnected) { + this.setState({ + isConnected, + }); + }, + render() { + return ( + {this.state.isConnected ? 'Online' : 'Offline'} + ); + } +}); + +const NetInfoExample = React.createClass({ + statics: { + title: '', + description: 'Monitor network status.' + }, + + getInitialState() { + return { + isMetered: null, + }; + }, + render() { + return ( + + Is Connected: + Current Connection Type: + Connection History: + + + Click to see if connection is metered: {this.state.isMetered} + + + + ); + }, + isConnectionMetered: function() { + NetInfo.isConnectionMetered((isConnectionMetered) => { + this.setState({ + isMetered: isConnectionMetered ? 'Is Metered' : 'Is Not Metered', + }); + }); + } +}); + +module.exports = NetInfoExample; diff --git a/Examples/UIExplorer/NetInfoExample.js b/Examples/UIExplorer/NetInfoExample.ios.js similarity index 100% rename from Examples/UIExplorer/NetInfoExample.js rename to Examples/UIExplorer/NetInfoExample.ios.js diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 7d57332c2..b3b0fc4d5 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -43,6 +43,7 @@ var APIS = [ require('./IntentAndroidExample.android'), require('./LayoutEventsExample'), require('./LayoutExample'), + require('./NetInfoExample.android'), require('./PanResponderExample'), require('./PointerEventsExample'), require('./TimerExample'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 940bd79ba..2f937cbeb 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -68,7 +68,7 @@ var APIS = [ require('./CameraRollExample.ios'), require('./GeolocationExample'), require('./LayoutExample'), - require('./NetInfoExample'), + require('./NetInfoExample.ios'), require('./PanResponderExample'), require('./PointerEventsExample'), require('./PushNotificationIOSExample'), diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml index b69776f9a..40bbd20f5 100644 --- a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.facebook.react.uiapp" > + ; + +var _subscriptions = new Map(); + +if (Platform.OS === 'ios') { + var _isConnected = function( + reachability: ReachabilityStateIOS + ): bool { + return reachability !== 'none' && + reachability !== 'unknown'; + }; +} else if (Platform.OS === 'android') { + var _isConnected = function( + connectionType: ConnectivityStateAndroid + ): bool { + return connectionType !== 'NONE' && connectionType !== 'UNKNOWN'; + }; +} + +var _isConnectedSubscriptions = new Map(); + /** * NetInfo exposes info about online/offline status * @@ -84,6 +104,10 @@ type ConnectivityStateAndroid = $Enum<{ * * ### Android * + * To request network info, you need to add the following line to your + * app's `AndroidManifest.xml`: + * + * `` * Asynchronously determine if the device is connected and details about that connection. * * Android Connectivity Types @@ -135,35 +159,15 @@ type ConnectivityStateAndroid = $Enum<{ * ); * ``` */ - -var _subscriptions = new Map(); - -if (Platform.OS === 'ios') { - var _isConnected = function( - reachability: ReachabilityStateIOS - ): bool { - return reachability !== 'none' && - reachability !== 'unknown'; - }; -} else if (Platform.OS === 'android') { - var _isConnected = function( - connectionType: ConnectivityStateAndroid - ): bool { - return connectionType !== 'NONE' && connectionType !== 'UNKNOWN'; - }; -} - -var _isConnectedSubscriptions = new Map(); - var NetInfo = { addEventListener: function ( eventName: ChangeEventName, handler: Function ): void { var listener = RCTDeviceEventEmitter.addListener( - DEVICE_REACHABILITY_EVENT, + DEVICE_CONNECTIVITY_EVENT, (appStateData) => { - handler(appStateData.network_reachability); + handler(appStateData.network_info); } ); _subscriptions.set(handler, listener); @@ -183,7 +187,7 @@ var NetInfo = { fetch: function(): Promise { return new Promise((resolve, reject) => { - RCTNetInfo.getCurrentReachability( + RCTNetInfo.getCurrentConnectivity( function(resp) { resolve(resp.network_info); }, diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index 5aab64432..78e1c3ab7 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -51,7 +51,7 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; - [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkDidChange" + [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}]; } } @@ -87,7 +87,7 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC #pragma mark - Public API // TODO: remove error callback - not needed except by Subscribable interface -RCT_EXPORT_METHOD(getCurrentReachability:(RCTResponseSenderBlock)getSuccess +RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTResponseSenderBlock)getSuccess withErrorCallback:(__unused RCTResponseSenderBlock)getError) { getSuccess(@[@{@"network_info": _status}]); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/netinfo/ConnectivityModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/netinfo/ConnectivityModule.java new file mode 100644 index 000000000..00414d86a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/netinfo/ConnectivityModule.java @@ -0,0 +1,154 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.modules.netinfo; + +import javax.annotation.Nullable; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.v4.net.ConnectivityManagerCompat; + +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + +/** + * Module that monitors and provides information about the connectivity state of the device. + */ +public class ConnectivityModule extends ReactContextBaseJavaModule + implements LifecycleEventListener { + + private static final String CONNECTION_TYPE_NONE = "NONE"; + private static final String CONNECTION_TYPE_UNKNOWN = "UNKNOWN"; + + private final ConnectivityManager mConnectivityManager; + private final ConnectivityManagerCompat mConnectivityManagerCompat; + + private String mConnectivity; + private @Nullable ConnectivityBroadcastReceiver mConnectivityBroadcastReceiver; + + public ConnectivityModule(ReactApplicationContext reactContext) { + super(reactContext); + mConnectivityManager = + (ConnectivityManager) reactContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mConnectivityManagerCompat = new ConnectivityManagerCompat(); + mConnectivity = ""; + } + + @Override + public void onHostResume() { + maybeRegisterReceiver(); + updateAndSendConnectionType(); + } + + @Override + public void onHostPause() { + maybeUnregisterReceiver(); + } + + @Override + public void onHostDestroy() { + } + + @Override + public void initialize() { + getReactApplicationContext().addLifecycleEventListener(this); + maybeRegisterReceiver(); + updateAndSendConnectionType(); + } + + @Override + public void onCatalystInstanceDestroy() { + maybeUnregisterReceiver(); + } + + @Override + public String getName() { + return "NetInfo"; + } + + @ReactMethod + public void getCurrentConnectivity(Callback successCallback, Callback errorCallback) { + successCallback.invoke(createConnectivityEventMap()); + } + + @ReactMethod + public void isConnectionMetered(Callback successCallback) { + successCallback.invoke(mConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager)); + } + + private void maybeRegisterReceiver() { + if (mConnectivityBroadcastReceiver != null) { + return; + } + mConnectivityBroadcastReceiver = new ConnectivityBroadcastReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + getReactApplicationContext().registerReceiver(mConnectivityBroadcastReceiver, filter); + } + + private void maybeUnregisterReceiver() { + if (mConnectivityBroadcastReceiver == null) { + return; + } + getReactApplicationContext().unregisterReceiver(mConnectivityBroadcastReceiver); + mConnectivityBroadcastReceiver = null; + mConnectivity = ""; + } + + private void updateAndSendConnectionType() { + String currentConnectivity = getCurrentConnectionType(); + // It is possible to get multiple broadcasts for the same connectivity change, so we only + // update and send an event when the connectivity has indeed changed. + if (!currentConnectivity.equalsIgnoreCase(mConnectivity)) { + mConnectivity = currentConnectivity; + sendConnectivityChangedEvent(); + } + } + + private String getCurrentConnectionType() { + NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnected()) { + return CONNECTION_TYPE_NONE; + } else if (ConnectivityManager.isNetworkTypeValid(networkInfo.getType())) { + return networkInfo.getTypeName().toUpperCase(); + } else { + return CONNECTION_TYPE_UNKNOWN; + } + } + private void sendConnectivityChangedEvent() { + getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class) + .emit("networkStatusDidChange", createConnectivityEventMap()); + } + + private WritableMap createConnectivityEventMap() { + WritableMap event = new WritableNativeMap(); + event.putString("network_info", mConnectivity); + return event; + } + + /** + * Class that receives intents whenever the connection type changes. + * NB: It is possible on some devices to receive certain connection type changes multiple times. + */ + private class ConnectivityBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + updateAndSendConnectionType(); + } + } + } +} 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 a0e90db20..bf91bade6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -20,6 +20,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.modules.fresco.FrescoModule; import com.facebook.react.modules.intent.IntentModule; import com.facebook.react.modules.location.LocationModule; +import com.facebook.react.modules.netinfo.ConnectivityModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; @@ -54,6 +55,7 @@ public class MainReactPackage implements ReactPackage { new IntentModule(reactContext), new LocationModule(reactContext), new NetworkingModule(reactContext), + new ConnectivityModule(reactContext), new WebSocketModule(reactContext), new ToastModule(reactContext)); }