// Copyright 2004-present Facebook. All Rights Reserved. package com.facebook.react.devsupport; import javax.annotation.Nullable; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import android.os.Handler; import android.os.Looper; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Inspector; 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 okio.Buffer; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class InspectorPackagerConnection { private static final String TAG = "InspectorPackagerConnection"; private final Connection mConnection; private final Map<String, Inspector.LocalConnection> mInspectorConnections; public InspectorPackagerConnection(String url) { mConnection = new Connection(url); mInspectorConnections = new HashMap<>(); } public void connect() { mConnection.connect(); } public void closeQuietly() { mConnection.close(); } public void sendOpenEvent(String pageId) { try { JSONObject payload = makePageIdPayload(pageId); sendEvent("open", payload); } catch (JSONException | IOException e) { FLog.e(TAG, "Failed to open page", e); } } void handleProxyMessage(JSONObject message) throws JSONException, IOException { String event = message.getString("event"); switch (event) { case "getPages": sendEvent("getPages", getPages()); break; case "wrappedEvent": handleWrappedEvent(message.getJSONObject("payload")); break; case "connect": handleConnect(message.getJSONObject("payload")); break; case "disconnect": handleDisconnect(message.getJSONObject("payload")); break; default: throw new IllegalArgumentException("Unknown event: " + event); } } void closeAllConnections() { for (Map.Entry<String, Inspector.LocalConnection> entry : mInspectorConnections.entrySet()) { entry.getValue().disconnect(); } mInspectorConnections.clear(); } private void handleConnect(JSONObject payload) throws JSONException, IOException { final String pageId = payload.getString("pageId"); Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId); if (inspectorConnection != null) { throw new IllegalStateException("Already connected: " + pageId); } try { // TODO: Use strings for id's too inspectorConnection = Inspector.connect(Integer.parseInt(pageId), new Inspector.RemoteConnection() { @Override public void onMessage(String message) { try { sendWrappedEvent(pageId, message); } catch (IOException | JSONException e) { FLog.w(TAG, "Couldn't send event to packager", e); } } @Override public void onDisconnect() { try { mInspectorConnections.remove(pageId); sendEvent("disconnect", makePageIdPayload(pageId)); } catch (IOException | JSONException e) { FLog.w(TAG, "Couldn't send event to packager", e); } } }); mInspectorConnections.put(pageId, inspectorConnection); } catch (Exception e) { FLog.w(TAG, "Failed to open page: " + pageId, e); sendEvent("disconnect", makePageIdPayload(pageId)); } } private void handleDisconnect(JSONObject payload) throws JSONException { final String pageId = payload.getString("pageId"); Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId); if (inspectorConnection == null) { return; } inspectorConnection.disconnect(); } private void handleWrappedEvent(JSONObject payload) throws JSONException, IOException { final String pageId = payload.getString("pageId"); String wrappedEvent = payload.getString("wrappedEvent"); Inspector.LocalConnection inspectorConnection = mInspectorConnections.get(pageId); if (inspectorConnection == null) { throw new IllegalStateException("Not connected: " + pageId); } inspectorConnection.sendMessage(wrappedEvent); } private JSONArray getPages() throws JSONException { List<Inspector.Page> pages = Inspector.getPages(); JSONArray array = new JSONArray(); for (Inspector.Page page : pages) { JSONObject jsonPage = new JSONObject(); jsonPage.put("id", String.valueOf(page.getId())); jsonPage.put("title", page.getTitle()); array.put(jsonPage); } return array; } private void sendWrappedEvent(String pageId, String message) throws IOException, JSONException { JSONObject payload = new JSONObject(); payload.put("pageId", pageId); payload.put("wrappedEvent", message); sendEvent("wrappedEvent", payload); } private void sendEvent(String name, Object payload) throws JSONException, IOException { JSONObject jsonMessage = new JSONObject(); jsonMessage.put("event", name); jsonMessage.put("payload", payload); mConnection.send(jsonMessage); } private JSONObject makePageIdPayload(String pageId) throws JSONException { JSONObject payload = new JSONObject(); payload.put("pageId", pageId); return payload; } private class Connection implements WebSocketListener { private static final int RECONNECT_DELAY_MS = 2000; private final String mUrl; private @Nullable WebSocket mWebSocket; private final Handler mHandler; private boolean mClosed; private boolean mSuppressConnectionErrors; public Connection(String url) { mUrl = url; mHandler = new Handler(Looper.getMainLooper()); } @Override public void onOpen(WebSocket webSocket, Response response) { mWebSocket = webSocket; } @Override public void onFailure(IOException e, Response response) { if (mWebSocket != null) { abort("Websocket exception", e); } if (!mClosed) { reconnect(); } } @Override public void onMessage(ResponseBody message) throws IOException { try { handleProxyMessage(new JSONObject(message.string())); } catch (JSONException e) { throw new IOException(e); } finally { message.close(); } } @Override public void onPong(Buffer payload) { } @Override public void onClose(int code, String reason) { mWebSocket = null; closeAllConnections(); if (!mClosed) { reconnect(); } } public void connect() { if (mClosed) { throw new IllegalStateException("Can't connect closed client"); } OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read .build(); Request request = new Request.Builder().url(mUrl).build(); WebSocketCall call = WebSocketCall.create(httpClient, request); call.enqueue(this); } private void reconnect() { if (mClosed) { throw new IllegalStateException("Can't reconnect closed client"); } if (!mSuppressConnectionErrors) { FLog.w(TAG, "Couldn't connect to packager, will silently retry"); mSuppressConnectionErrors = true; } mHandler.postDelayed( new Runnable() { @Override public void run() { // check that we haven't been closed in the meantime if (!mClosed) { connect(); } } }, RECONNECT_DELAY_MS); } public void close() { mClosed = true; if (mWebSocket != null) { try { mWebSocket.close(1000, "End of session"); } catch (IOException e) { // swallow, no need to handle it here } mWebSocket = null; } } public void send(JSONObject object) throws IOException { if (mWebSocket == null) { return; } mWebSocket.sendMessage(RequestBody.create(WebSocket.TEXT, object.toString())); } private void abort(String message, Throwable cause) { FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause); closeAllConnections(); closeWebSocketQuietly(); } private void closeWebSocketQuietly() { if (mWebSocket != null) { try { mWebSocket.close(1000, "End of session"); } catch (IOException e) { // swallow, no need to handle it here } mWebSocket = null; } } } }