Java tutorial
/** * This is a modified version of React Native's WebSocketModule that * understand binary blobs. */ /** * 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.silklabs.react.blobs; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.util.Base64; import java.io.IOException; import java.lang.IllegalStateException; import javax.annotation.Nullable; 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.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.modules.core.DeviceEventManagerModule; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.ws.WebSocket; import okhttp3.ws.WebSocketCall; import okhttp3.ws.WebSocketListener; import java.net.URISyntaxException; import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import okio.Buffer; import okio.ByteString; public class WebSocketModule extends ReactContextBaseJavaModule { private Map<Integer, WebSocket> mWebSocketConnections = new HashMap<>(); private Map<Integer, Boolean> mBlobsEnabled = new HashMap<>(); private ReactContext mReactContext; private BlobProvider mBlobProvider; 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, @Nullable final ReadableArray protocols, @Nullable final ReadableMap headers, final int id) { OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS).readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read .build(); Request.Builder builder = new Request.Builder().tag(id).url(url); if (headers != null) { ReadableMapKeySetIterator iterator = headers.keySetIterator(); if (!headers.hasKey("origin")) { builder.addHeader("origin", setDefaultOrigin(url)); } while (iterator.hasNextKey()) { String key = iterator.nextKey(); if (ReadableType.String.equals(headers.getType(key))) { builder.addHeader(key, headers.getString(key)); } else { FLog.w(ReactConstants.TAG, "Ignoring: requested " + key + ", value not a string"); } } } else { builder.addHeader("origin", setDefaultOrigin(url)); } if (protocols != null && protocols.size() > 0) { StringBuilder protocolsValue = new StringBuilder(""); for (int i = 0; i < protocols.size(); i++) { String v = protocols.getString(i).trim(); if (!v.isEmpty() && !v.contains(",")) { protocolsValue.append(v); protocolsValue.append(","); } } if (protocolsValue.length() > 0) { protocolsValue.replace(protocolsValue.length() - 1, protocolsValue.length(), ""); builder.addHeader("Sec-WebSocket-Protocol", protocolsValue.toString()); } } WebSocketCall.create(client, builder.build()).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(ResponseBody response) throws IOException { WritableMap params = Arguments.createMap(); params.putInt("id", id); if (mBlobsEnabled.containsKey(id) && mBlobsEnabled.get(id) && response.contentType() == WebSocket.BINARY) { byte[] data; try { data = response.source().readByteArray(); } catch (IOException e) { notifyWebSocketFailed(id, e.getMessage()); return; } WritableMap blob = Arguments.createMap(); blob.putString("blobId", BlobModule.store(data)); blob.putInt("offset", 0); blob.putInt("size", data.length); params.putMap("data", blob); params.putString("type", "blob"); } else { String message; try { if (response.contentType() == WebSocket.BINARY) { message = Base64.encodeToString(response.source().readByteArray(), Base64.NO_WRAP); } else { message = response.source().readUtf8(); } } catch (IOException e) { notifyWebSocketFailed(id, e.getMessage()); return; } params.putString("data", message); params.putString("type", response.contentType() == WebSocket.BINARY ? "binary" : "text"); } try { response.source().close(); } catch (IOException e) { FLog.e(ReactConstants.TAG, "Could not close BufferedSource for WebSocket id " + id, e); } sendEvent("websocketMessage", params); } }); // Trigger shutdown of the dispatcher's executor so this process can exit cleanly client.dispatcher().executorService().shutdown(); } @ReactMethod public void close(int code, String reason, int id) { WebSocket client = mWebSocketConnections.get(id); if (client == null) { // WebSocket is already closed // Don't do anything, mirror the behaviour on web FLog.w(ReactConstants.TAG, "Cannot close WebSocket. Unknown WebSocket id " + id); return; } try { client.close(code, reason); mWebSocketConnections.remove(id); mBlobsEnabled.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(RequestBody.create(WebSocket.TEXT, message)); } catch (IOException | IllegalStateException e) { notifyWebSocketFailed(id, e.getMessage()); } } @ReactMethod public void sendBinary(String base64String, 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(RequestBody.create(WebSocket.BINARY, ByteString.decodeBase64(base64String))); } catch (IOException | IllegalStateException e) { notifyWebSocketFailed(id, e.getMessage()); } } @ReactMethod public void sendBlob(ReadableMap blob, 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); } byte[] data = BlobModule.resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size")); try { client.sendMessage(RequestBody.create(WebSocket.BINARY, data)); } catch (IOException | IllegalStateException 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); } /** * Set a default origin * * @param Websocket connection endpoint * @return A string of the endpoint converted to HTTP protocol */ private static String setDefaultOrigin(String uri) { try { String defaultOrigin; String scheme = ""; URI requestURI = new URI(uri); if (requestURI.getScheme().equals("wss")) { scheme += "https"; } else if (requestURI.getScheme().equals("ws")) { scheme += "http"; } if (requestURI.getPort() != -1) { defaultOrigin = String.format("%s://%s:%s", scheme, requestURI.getHost(), requestURI.getPort()); } else { defaultOrigin = String.format("%s://%s/", scheme, requestURI.getHost()); } return defaultOrigin; } catch (URISyntaxException e) { throw new IllegalArgumentException("Unable to set " + uri + " as default origin header."); } } @ReactMethod public void setBinaryType(String binaryType, int id) { mBlobsEnabled.put(id, binaryType.equals("blob")); } @Override public boolean canOverrideExistingModule() { return true; } }