From f4857a6d4237014cd66c213804ae54a99d71efaa Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Wed, 7 Oct 2015 08:28:34 -0700 Subject: [PATCH] Implement WebSocket module for Android. Fixes #2837 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The JavaScript code for Android is same as the iOS counterpart, I just added few new lines and used arrow functions instead of binding `this`. Closes https://github.com/facebook/react-native/pull/2839 Reviewed By: @​svcscm, @vjeux Differential Revision: D2498703 Pulled By: @mkonicek fb-gh-sync-id: 3fe958dd5af0efba00df07515f8e33b5d87eb05b --- .../RCTWebSocket.xcodeproj/project.pbxproj | 12 +- ...ebSocketManager.h => RCTWebSocketModule.h} | 2 +- ...ebSocketManager.m => RCTWebSocketModule.m} | 6 +- Libraries/WebSocket/WebSocket.android.js | 39 ---- Libraries/WebSocket/WebSocket.ios.js | 126 ------------- Libraries/WebSocket/WebSocket.js | 124 +++++++++++++ Libraries/WebSocket/WebSocketBase.js | 2 - Libraries/WebSocket/WebSocketEvent.js | 29 +++ .../modules/websocket/WebSocketModule.java | 170 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 2 + 10 files changed, 335 insertions(+), 177 deletions(-) rename Libraries/WebSocket/{RCTWebSocketManager.h => RCTWebSocketModule.h} (85%) rename Libraries/WebSocket/{RCTWebSocketManager.m => RCTWebSocketModule.m} (95%) delete mode 100644 Libraries/WebSocket/WebSocket.android.js delete mode 100644 Libraries/WebSocket/WebSocket.ios.js create mode 100644 Libraries/WebSocket/WebSocket.js create mode 100644 Libraries/WebSocket/WebSocketEvent.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java diff --git a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj index e1862d581..62dbee6a7 100644 --- a/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj +++ b/Libraries/WebSocket/RCTWebSocket.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */; }; 1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */; }; - 3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */; }; + 3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -18,8 +18,8 @@ 1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; 3C86DF461ADF2C930047B81A /* libRCTWebSocket.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocket.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketManager.h; sourceTree = ""; }; - 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketManager.m; sourceTree = ""; tabWidth = 2; }; + 3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketModule.h; sourceTree = ""; }; + 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketModule.m; sourceTree = ""; tabWidth = 2; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,8 +40,8 @@ 1338BBDD1B04ACC80064A9C9 /* RCTSRWebSocket.m */, 1338BBDE1B04ACC80064A9C9 /* RCTWebSocketExecutor.h */, 1338BBDF1B04ACC80064A9C9 /* RCTWebSocketExecutor.m */, - 3C86DF7A1ADF695F0047B81A /* RCTWebSocketManager.h */, - 3C86DF7B1ADF695F0047B81A /* RCTWebSocketManager.m */, + 3C86DF7A1ADF695F0047B81A /* RCTWebSocketModule.h */, + 3C86DF7B1ADF695F0047B81A /* RCTWebSocketModule.m */, 3C86DF471ADF2C930047B81A /* Products */, ); indentWidth = 2; @@ -112,7 +112,7 @@ buildActionMask = 2147483647; files = ( 1338BBE01B04ACC80064A9C9 /* RCTSRWebSocket.m in Sources */, - 3C86DF7C1ADF695F0047B81A /* RCTWebSocketManager.m in Sources */, + 3C86DF7C1ADF695F0047B81A /* RCTWebSocketModule.m in Sources */, 1338BBE11B04ACC80064A9C9 /* RCTWebSocketExecutor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Libraries/WebSocket/RCTWebSocketManager.h b/Libraries/WebSocket/RCTWebSocketModule.h similarity index 85% rename from Libraries/WebSocket/RCTWebSocketManager.h rename to Libraries/WebSocket/RCTWebSocketModule.h index 196486f25..ee176a9f2 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.h +++ b/Libraries/WebSocket/RCTWebSocketModule.h @@ -9,6 +9,6 @@ #import "RCTBridgeModule.h" -@interface RCTWebSocketManager : NSObject +@interface RCTWebSocketModule : NSObject @end diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketModule.m similarity index 95% rename from Libraries/WebSocket/RCTWebSocketManager.m rename to Libraries/WebSocket/RCTWebSocketModule.m index 039dd7f22..24066439f 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.m +++ b/Libraries/WebSocket/RCTWebSocketModule.m @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTWebSocketManager.h" +#import "RCTWebSocketModule.h" #import "RCTBridge.h" #import "RCTEventDispatcher.h" @@ -29,11 +29,11 @@ @end -@interface RCTWebSocketManager () +@interface RCTWebSocketModule () @end -@implementation RCTWebSocketManager +@implementation RCTWebSocketModule { RCTSparseArray *_sockets; } diff --git a/Libraries/WebSocket/WebSocket.android.js b/Libraries/WebSocket/WebSocket.android.js deleted file mode 100644 index 301f92f49..000000000 --- a/Libraries/WebSocket/WebSocket.android.js +++ /dev/null @@ -1,39 +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 WebSocket - * - */ -'use strict'; - -var WebSocketBase = require('WebSocketBase'); - -class WebSocket extends WebSocketBase { - - connectToSocketImpl(url: string): void { - console.warn('WebSocket is not yet supported on Android'); - } - - closeConnectionImpl(): void{ - console.warn('WebSocket is not yet supported on Android'); - } - - cancelConnectionImpl(): void { - console.warn('WebSocket is not yet supported on Android'); - } - - sendStringImpl(message: string): void { - console.warn('WebSocket is not yet supported on Android'); - } - - sendArrayBufferImpl(): void { - console.warn('WebSocket is not yet supported on Android'); - } -} - -module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js deleted file mode 100644 index c2318b497..000000000 --- a/Libraries/WebSocket/WebSocket.ios.js +++ /dev/null @@ -1,126 +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 WebSocket - * - */ -'use strict'; - -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTWebSocketManager = require('NativeModules').WebSocketManager; - -var WebSocketBase = require('WebSocketBase'); - -class Event { - constructor(type) { - this.type = type.toString(); - } -} - -class MessageEvent extends Event { - constructor(type, eventInitDict) { - super(type); - Object.assign(this, eventInitDict); - } -} - -var WebSocketId = 0; - -class WebSocket extends WebSocketBase { - _socketId: number; - _subs: any; - - connectToSocketImpl(url: string): void { - this._socketId = WebSocketId++; - RCTWebSocketManager.connect(url, this._socketId); - this._registerEvents(this._socketId); - } - - closeConnectionImpl(): void { - RCTWebSocketManager.close(this._socketId); - } - - cancelConnectionImpl(): void { - RCTWebSocketManager.close(this._socketId); - } - - sendStringImpl(message: string): void { - RCTWebSocketManager.send(message, this._socketId); - } - - sendArrayBufferImpl(): void { - // TODO - console.warn('Sending ArrayBuffers is not yet supported'); - } - - _unregisterEvents(): void { - this._subs.forEach(e => e.remove()); - this._subs = []; - } - - _registerEvents(id: number): void { - this._subs = [ - RCTDeviceEventEmitter.addListener( - 'websocketMessage', - function(ev) { - if (ev.id !== id) { - return; - } - var event = new MessageEvent('message', { - data: ev.data - }); - this.onmessage && this.onmessage(event); - this.dispatchEvent(event); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketOpen', - function(ev) { - if (ev.id !== id) { - return; - } - this.readyState = this.OPEN; - var event = new Event('open'); - this.onopen && this.onopen(event); - this.dispatchEvent(event); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketClosed', - function(ev) { - if (ev.id !== id) { - return; - } - this.readyState = this.CLOSED; - var event = new Event('close'); - this.onclose && this.onclose(event); - this.dispatchEvent(event); - this._unregisterEvents(); - RCTWebSocketManager.close(id); - }.bind(this) - ), - RCTDeviceEventEmitter.addListener( - 'websocketFailed', - function(ev) { - if (ev.id !== id) { - return; - } - var event = new Event('error'); - event.message = ev.message; - this.onerror && this.onerror(event); - this.dispatchEvent(event); - this._unregisterEvents(); - RCTWebSocketManager.close(id); - }.bind(this) - ) - ]; - } - -} - -module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.js b/Libraries/WebSocket/WebSocket.js new file mode 100644 index 000000000..5751f0f84 --- /dev/null +++ b/Libraries/WebSocket/WebSocket.js @@ -0,0 +1,124 @@ +/** + * 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 WebSocket + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTWebSocketModule = require('NativeModules').WebSocketModule; + +var Platform = require('Platform'); +var WebSocketBase = require('WebSocketBase'); +var WebSocketEvent = require('WebSocketEvent'); + +var WebSocketId = 0; +var CLOSE_NORMAL = 1000; + +/** + * Browser-compatible WebSockets implementation. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket + */ +class WebSocket extends WebSocketBase { + _socketId: number; + _subs: any; + + connectToSocketImpl(url: string): void { + this._socketId = WebSocketId++; + + RCTWebSocketModule.connect(url, this._socketId); + + this._registerEvents(this._socketId); + } + + closeConnectionImpl(code?: number, reason?: string): void { + this._closeWebSocket(this._socketId, code, reason); + } + + cancelConnectionImpl(): void { + this._closeWebSocket(this._socketId); + } + + sendStringImpl(message: string): void { + RCTWebSocketModule.send(message, this._socketId); + } + + sendArrayBufferImpl(): void { + // TODO + console.warn('Sending ArrayBuffers is not yet supported'); + } + + _closeWebSocket(id: number, code?: number, reason?: string): void { + if (Platform.OS === 'android') { + /* + * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + */ + var statusCode = typeof code === 'number' ? code : CLOSE_NORMAL; + var closeReason = typeof reason === 'string' ? reason : ''; + RCTWebSocketModule.close(statusCode, closeReason, id); + } else { + RCTWebSocketModule.close(id); + } + } + + _unregisterEvents(): void { + this._subs.forEach(e => e.remove()); + this._subs = []; + } + + _registerEvents(id: number): void { + this._subs = [ + RCTDeviceEventEmitter.addListener('websocketMessage', ev => { + if (ev.id !== id) { + return; + } + var event = new WebSocketEvent('message', { + data: ev.data + }); + this.onmessage && this.onmessage(event); + this.dispatchEvent(event); + }), + RCTDeviceEventEmitter.addListener('websocketOpen', ev => { + if (ev.id !== id) { + return; + } + this.readyState = this.OPEN; + var event = new WebSocketEvent('open'); + this.onopen && this.onopen(event); + this.dispatchEvent(event); + }), + RCTDeviceEventEmitter.addListener('websocketClosed', ev => { + if (ev.id !== id) { + return; + } + this.readyState = this.CLOSED; + var event = new WebSocketEvent('close'); + event.code = ev.code; + event.reason = ev.reason; + this.onclose && this.onclose(event); + this.dispatchEvent(event); + this._unregisterEvents(); + this._closeWebSocket(id); + }), + RCTDeviceEventEmitter.addListener('websocketFailed', ev => { + if (ev.id !== id) { + return; + } + var event = new WebSocketEvent('error'); + event.message = ev.message; + this.onerror && this.onerror(event); + this.dispatchEvent(event); + this._unregisterEvents(); + this._closeWebSocket(id); + }) + ]; + } +} + +module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocketBase.js b/Libraries/WebSocket/WebSocketBase.js index a4cca4184..aa4777c91 100644 --- a/Libraries/WebSocket/WebSocketBase.js +++ b/Libraries/WebSocket/WebSocketBase.js @@ -7,7 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule WebSocketBase - * */ 'use strict'; @@ -94,7 +93,6 @@ class WebSocketBase extends EventTarget { sendArrayBufferImpl(): void { throw new Error('Subclass must define sendArrayBufferImpl method'); } - } module.exports = WebSocketBase; diff --git a/Libraries/WebSocket/WebSocketEvent.js b/Libraries/WebSocket/WebSocketEvent.js new file mode 100644 index 000000000..27ca093bc --- /dev/null +++ b/Libraries/WebSocket/WebSocketEvent.js @@ -0,0 +1,29 @@ +/** + * 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 WebSocketEvent + */ + +'use strict'; + +/** + * Event object passed to the `onopen`, `onclose`, `onmessage`, `onerror` + * callbacks of `WebSocket`. + * + * The `type` property is "open", "close", "message", "error" respectively. + * + * In case of "message", the `data` property contains the incoming data. + */ +class WebSocketEvent { + constructor(type, eventInitDict) { + this.type = type.toString(); + Object.assign(this, eventInitDict); + } +} + +module.exports = WebSocketEvent; diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java new file mode 100644 index 000000000..0e5ce7e9b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -0,0 +1,170 @@ +/** + * 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.websocket; + +import java.io.IOException; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; +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.WritableMap; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ws.WebSocket; +import com.squareup.okhttp.ws.WebSocketCall; +import com.squareup.okhttp.ws.WebSocketListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okio.Buffer; +import okio.BufferedSource; + +public class WebSocketModule extends ReactContextBaseJavaModule { + + private Map mWebSocketConnections = new HashMap<>(); + private ReactContext mReactContext; + + public WebSocketModule(ReactApplicationContext context) { + super(context); + mReactContext = context; + } + + private void sendEvent(String eventName, WritableMap params) { + mReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } + + @Override + public String getName() { + return "WebSocketModule"; + } + + @ReactMethod + public void connect(final String url, final int id) { + OkHttpClient client = new OkHttpClient(); + + client.setConnectTimeout(10, TimeUnit.SECONDS); + client.setWriteTimeout(10, TimeUnit.SECONDS); + // Disable timeouts for read + client.setReadTimeout(0, TimeUnit.MINUTES); + + Request request = new Request.Builder() + .tag(id) + .url(url) + .build(); + + WebSocketCall.create(client, request).enqueue(new WebSocketListener() { + + @Override + public void onOpen(WebSocket webSocket, Response response) { + mWebSocketConnections.put(id, webSocket); + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + sendEvent("websocketOpen", params); + } + + @Override + public void onClose(int code, String reason) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putInt("code", code); + params.putString("reason", reason); + sendEvent("websocketClosed", params); + } + + @Override + public void onFailure(IOException e, Response response) { + notifyWebSocketFailed(id, e.getMessage()); + } + + @Override + public void onPong(Buffer buffer) { + } + + @Override + public void onMessage(BufferedSource bufferedSource, WebSocket.PayloadType payloadType) { + String message; + try { + message = bufferedSource.readUtf8(); + } catch (IOException e) { + notifyWebSocketFailed(id, e.getMessage()); + return; + } + try { + bufferedSource.close(); + } catch (IOException e) { + FLog.e( + ReactConstants.TAG, + "Could not close BufferedSource for WebSocket id " + id, + e); + } + + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("data", message); + sendEvent("websocketMessage", params); + } + }); + + // Trigger shutdown of the dispatcher's executor so this process can exit cleanly + client.getDispatcher().getExecutorService().shutdown(); + } + + @ReactMethod + public void close(int code, String reason, int id) { + WebSocket client = mWebSocketConnections.get(id); + if (client == null) { + // This is a programmer error + throw new RuntimeException("Cannot close WebSocket. Unknown WebSocket id " + id); + } + try { + client.close(code, reason); + mWebSocketConnections.remove(id); + } catch (Exception e) { + FLog.e( + ReactConstants.TAG, + "Could not close WebSocket connection for id " + id, + e); + } + } + + @ReactMethod + public void send(String message, int id) { + WebSocket client = mWebSocketConnections.get(id); + if (client == null) { + // This is a programmer error + throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id); + } + try { + client.sendMessage( + WebSocket.PayloadType.TEXT, + new Buffer().writeUtf8(message)); + } catch (IOException e) { + notifyWebSocketFailed(id, e.getMessage()); + } + } + + private void notifyWebSocketFailed(int id, String message) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("message", message); + sendEvent("websocketFailed", params); + } +} 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 f4b011d46..e8d7b93cd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -21,6 +21,7 @@ import com.facebook.react.modules.fresco.FrescoModule; import com.facebook.react.modules.network.NetworkingModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; +import com.facebook.react.modules.websocket.WebSocketModule; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.drawer.ReactDrawerLayoutManager; import com.facebook.react.views.image.ReactImageManager; @@ -47,6 +48,7 @@ public class MainReactPackage implements ReactPackage { new AsyncStorageModule(reactContext), new FrescoModule(reactContext), new NetworkingModule(reactContext), + new WebSocketModule(reactContext), new ToastModule(reactContext)); }