diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 149ba7a51ea..fd8b518ebda 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3272,15 +3272,15 @@ public class com/facebook/react/packagerconnection/PackagerConnectionSettings { public final class com/facebook/react/packagerconnection/ReconnectingWebSocket : okhttp3/WebSocketListener { public fun (Ljava/lang/String;Lcom/facebook/react/packagerconnection/ReconnectingWebSocket$MessageCallback;Lcom/facebook/react/packagerconnection/ReconnectingWebSocket$ConnectionCallback;)V - public fun closeQuietly ()V - public fun connect ()V + public final fun closeQuietly ()V + public final fun connect ()V public fun onClosed (Lokhttp3/WebSocket;ILjava/lang/String;)V public fun onFailure (Lokhttp3/WebSocket;Ljava/lang/Throwable;Lokhttp3/Response;)V public fun onMessage (Lokhttp3/WebSocket;Ljava/lang/String;)V public fun onMessage (Lokhttp3/WebSocket;Lokio/ByteString;)V public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V - public fun sendMessage (Ljava/lang/String;)V - public fun sendMessage (Lokio/ByteString;)V + public final fun sendMessage (Ljava/lang/String;)V + public final fun sendMessage (Lokio/ByteString;)V } public abstract interface class com/facebook/react/packagerconnection/ReconnectingWebSocket$ConnectionCallback { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.java deleted file mode 100644 index 02bcd557453..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.packagerconnection; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import java.io.IOException; -import java.nio.channels.ClosedChannelException; -import java.util.concurrent.TimeUnit; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okio.ByteString; - -/** A wrapper around WebSocketClient that reconnects automatically */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public final class ReconnectingWebSocket extends WebSocketListener { - private static final String TAG = ReconnectingWebSocket.class.getSimpleName(); - - private static final int RECONNECT_DELAY_MS = 2000; - - public interface MessageCallback { - void onMessage(String text); - - void onMessage(ByteString bytes); - } - - public interface ConnectionCallback { - void onConnected(); - - void onDisconnected(); - } - - private final String mUrl; - private final Handler mHandler; - private final OkHttpClient mOkHttpClient; - private boolean mClosed = false; - private boolean mSuppressConnectionErrors; - private @Nullable WebSocket mWebSocket; - private @Nullable MessageCallback mMessageCallback; - private @Nullable ConnectionCallback mConnectionCallback; - - public ReconnectingWebSocket( - String url, - @Nullable MessageCallback messageCallback, - @Nullable ConnectionCallback connectionCallback) { - super(); - mUrl = url; - mMessageCallback = messageCallback; - mConnectionCallback = connectionCallback; - mHandler = new Handler(Looper.getMainLooper()); - mOkHttpClient = - new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read - .build(); - } - - public void connect() { - if (mClosed) { - throw new IllegalStateException("Can't connect closed client"); - } - - Request request = new Request.Builder().url(mUrl).build(); - mOkHttpClient.newWebSocket(request, 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(); - mMessageCallback = null; - - if (mConnectionCallback != null) { - mConnectionCallback.onDisconnected(); - } - } - - private void closeWebSocketQuietly() { - if (mWebSocket != null) { - try { - mWebSocket.close(1000, "End of session"); - } catch (Exception 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; - - if (mConnectionCallback != null) { - mConnectionCallback.onConnected(); - } - } - - @Override - public synchronized void onFailure( - @Nullable WebSocket webSocket, Throwable t, @Nullable Response response) { - if (mWebSocket != null) { - abort("Websocket exception", t); - } - if (!mClosed) { - if (mConnectionCallback != null) { - mConnectionCallback.onDisconnected(); - } - reconnect(); - } - } - - @Override - public synchronized void onMessage(WebSocket webSocket, String text) { - if (mMessageCallback != null) { - mMessageCallback.onMessage(text); - } - } - - @Override - public synchronized void onMessage(WebSocket webSocket, ByteString bytes) { - if (mMessageCallback != null) { - mMessageCallback.onMessage(bytes); - } - } - - @Override - public synchronized void onClosed(WebSocket webSocket, int code, String reason) { - mWebSocket = null; - if (!mClosed) { - if (mConnectionCallback != null) { - mConnectionCallback.onDisconnected(); - } - reconnect(); - } - } - - public synchronized void sendMessage(String message) throws IOException { - if (mWebSocket != null) { - mWebSocket.send(message); - } else { - throw new ClosedChannelException(); - } - } - - public synchronized void sendMessage(ByteString message) throws IOException { - if (mWebSocket != null) { - mWebSocket.send(message); - } else { - throw new ClosedChannelException(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt new file mode 100644 index 00000000000..f60dab38a18 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.packagerconnection + +import android.os.Handler +import android.os.Looper +import com.facebook.common.logging.FLog +import java.io.IOException +import java.nio.channels.ClosedChannelException +import java.util.concurrent.TimeUnit +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString + +/** A wrapper around WebSocketClient that reconnects automatically */ +public class ReconnectingWebSocket( + private val url: String, + private var messageCallback: MessageCallback?, + private val connectionCallback: ConnectionCallback? +) : WebSocketListener() { + + public interface MessageCallback { + public fun onMessage(text: String) + + public fun onMessage(bytes: ByteString) + } + + public interface ConnectionCallback { + public fun onConnected() + + public fun onDisconnected() + } + + private val handler = Handler(Looper.getMainLooper()) + private val okHttpClient: OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read + .build() + private var closed = false + private var suppressConnectionErrors = false + private var webSocket: WebSocket? = null + + public fun connect(): Unit { + check(!closed) { "Can't connect closed client" } + + val request = Request.Builder().url(url).build() + okHttpClient.newWebSocket(request, this) + } + + @Synchronized + private fun delayedReconnect() { + // check that we haven't been closed in the meantime + if (!closed) { + connect() + } + } + + private fun reconnect() { + check(!closed) { "Can't reconnect closed client" } + + if (!suppressConnectionErrors) { + FLog.w(TAG, "Couldn't connect to \"$url\", will silently retry") + suppressConnectionErrors = true + } + + handler.postDelayed({ delayedReconnect() }, RECONNECT_DELAY_MS) + } + + public fun closeQuietly(): Unit { + closed = true + closeWebSocketQuietly() + messageCallback = null + + connectionCallback?.onDisconnected() + } + + private fun closeWebSocketQuietly() { + try { + webSocket?.close(1_000, "End of session") + } catch (e: Exception) { + // swallow, no need to handle it here + } + webSocket = null + } + + private fun abort(message: String, cause: Throwable) { + FLog.e(TAG, "Error occurred, shutting down websocket connection: $message", cause) + closeWebSocketQuietly() + } + + @Synchronized + override fun onOpen(webSocket: WebSocket, response: Response) { + this.webSocket = webSocket + suppressConnectionErrors = false + + connectionCallback?.onConnected() + } + + @Synchronized + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + if (this.webSocket != null) { + abort("Websocket exception", t) + } + if (!closed) { + connectionCallback?.onDisconnected() + reconnect() + } + } + + @Synchronized + override fun onMessage(webSocket: WebSocket, text: String) { + messageCallback?.onMessage(text) + } + + @Synchronized + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + messageCallback?.onMessage(bytes) + } + + @Synchronized + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + this.webSocket = null + if (!closed) { + connectionCallback?.onDisconnected() + reconnect() + } + } + + @Synchronized + @Throws(IOException::class) + public fun sendMessage(message: String): Unit { + webSocket?.send(message) ?: throw ClosedChannelException() + } + + @Synchronized + @Throws(IOException::class) + public fun sendMessage(message: ByteString): Unit { + webSocket?.send(message) ?: throw ClosedChannelException() + } + + private companion object { + private val TAG: String = ReconnectingWebSocket::class.java.simpleName + + private const val RECONNECT_DELAY_MS = 2_000L + } +}