/** * 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.packagerconnection; import javax.annotation.Nullable; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.util.concurrent.TimeUnit; import android.os.Handler; import android.os.Looper; import com.facebook.common.logging.FLog; 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; /** * A wrapper around WebSocketClient that reconnects automatically */ final public class ReconnectingWebSocket implements WebSocketListener { private static final String TAG = ReconnectingWebSocket.class.getSimpleName(); private static final int RECONNECT_DELAY_MS = 2000; public interface MessageCallback { void onMessage(ResponseBody message); } private final String mUrl; private final Handler mHandler; private boolean mClosed = false; private boolean mSuppressConnectionErrors; private @Nullable WebSocket mWebSocket; private @Nullable MessageCallback mCallback; public ReconnectingWebSocket(String url, MessageCallback callback) { super(); mUrl = url; mCallback = callback; mHandler = new Handler(Looper.getMainLooper()); } 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 synchronized void delayedReconnect() { // check that we haven't been closed in the meantime if (!mClosed) { connect(); } } private void reconnect() { if (mClosed) { throw new IllegalStateException("Can't reconnect closed client"); } if (!mSuppressConnectionErrors) { FLog.w(TAG, "Couldn't connect to \"" + mUrl + "\", will silently retry"); mSuppressConnectionErrors = true; } mHandler.postDelayed( new Runnable() { @Override public void run() { delayedReconnect(); } }, RECONNECT_DELAY_MS); } public void closeQuietly() { mClosed = true; closeWebSocketQuietly(); mCallback = null; } 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; } } private void abort(String message, Throwable cause) { FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause); closeWebSocketQuietly(); } @Override public synchronized void onOpen(WebSocket webSocket, Response response) { mWebSocket = webSocket; mSuppressConnectionErrors = false; } @Override public synchronized void onFailure(IOException e, Response response) { if (mWebSocket != null) { abort("Websocket exception", e); } if (!mClosed) { reconnect(); } } @Override public synchronized void onMessage(ResponseBody message) { if (mCallback != null) { mCallback.onMessage(message); } } @Override public synchronized void onPong(Buffer payload) { } @Override public synchronized void onClose(int code, String reason) { mWebSocket = null; if (!mClosed) { reconnect(); } } public synchronized void sendMessage(RequestBody message) throws IOException { if (mWebSocket != null) { mWebSocket.sendMessage(message); } else { throw new ClosedChannelException(); } } }