-
Notifications
You must be signed in to change notification settings - Fork 532
feat: ✨ Native Websocket Plugin (uses okhttp) #1335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
bajrangCoder
merged 15 commits into
Acode-Foundation:main
from
UnschooledGamer:feat/native-websocket-plugin
Jun 20, 2025
Merged
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
55aab1d
feat: :sparkles: Native Websocket Plugin (uses okhttp)
UnschooledGamer 71481d6
update: readme of cordova websocket plugin for importing it.
UnschooledGamer 410e1df
Apply suggestions from code review
UnschooledGamer 86856b7
feat: enhance WebSocket plugin with custom headers and improved close…
UnschooledGamer 537df1f
feat: improve WebSocketInstance and WebSocketPlugin with enhanced log…
UnschooledGamer bf90618
Apply suggestions from code review
UnschooledGamer d6862a6
refactor: replace HashMap with ConcurrentHashMap for thread-safe WebS…
UnschooledGamer 48a831b
feat: enhance WebSocket close event handling
UnschooledGamer 4f27c2b
feat: enhance WebSocketInstance with binary message support and impro…
UnschooledGamer 92cf6c2
Merge branch 'main' into feat/native-websocket-plugin
UnschooledGamer 874a487
fix: add missing import for ByteString in WebSocketInstance
UnschooledGamer 7b65cb4
feat: add devcontainer configuration with Android SDK support
UnschooledGamer 2f98b4e
feat: enhance WebSocket plugin with binary message support and improv…
UnschooledGamer d60f21e
feat: improve WebSocket instance management on close event
UnschooledGamer 2277e40
fix: ensure WebSocket closes properly on closing event
UnschooledGamer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# Cordova Plugin: OkHttp WebSocket | ||
|
||
A Cordova plugin that uses [OkHttp](https://square.github.io/okhttp/) to provide WebSocket support in your Cordova app. | ||
It aims to mimic the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) in JavaScript, with additional features. | ||
|
||
## Features | ||
|
||
* ✅ WebSocket API-like interface | ||
* ✅ Event support: `onopen`, `onmessage`, `onerror`, `onclose` | ||
* ✅ `extensions` and `readyState` properties | ||
* ✅ `listClients()` to list active connections | ||
* ✅ Support for protocols | ||
* ✅ Support for Custom Headers. | ||
* ✅ Compatible with Cordova for Android | ||
|
||
--- | ||
|
||
## Usage | ||
|
||
### Import | ||
|
||
```javascript | ||
const WebSocketPlugin = cordova.websocket; | ||
``` | ||
|
||
### Connect to WebSocket | ||
|
||
```javascript | ||
WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"], headers) | ||
.then(ws => { | ||
ws.onopen = (e) => console.log("Connected!", e); | ||
ws.onmessage = (e) => console.log("Message:", e.data); | ||
ws.onerror = (e) => console.error("Error:", e); | ||
ws.onclose = (e) => console.log("Closed:", e); | ||
|
||
ws.send("Hello from Cordova!"); | ||
ws.close(); | ||
}) | ||
.catch(err => console.error("WebSocket connection failed:", err)); | ||
``` | ||
|
||
--- | ||
|
||
## API Reference | ||
|
||
### Methods | ||
|
||
* `WebSocketPlugin.connect(url, protocols, headers)` | ||
|
||
* Connects to a WebSocket server. | ||
* `url`: The WebSocket server URL. | ||
* `protocols`: (Optional) An array of subprotocol strings. | ||
* `headers` (object, optional): Custom headers as key-value pairs. | ||
* **Returns:** A Promise that resolves to a `WebSocketInstance`. | ||
* `WebSocketPlugin.listClients()` | ||
* Lists all stored webSocket instance IDs. | ||
* **Returns:** `Promise`that resolves to an array of `instanceId` strings. | ||
|
||
* `WebSocketPlugin.send(instanceId, message)` | ||
* same as `WebSocketInstance.send(message)` but needs `instanceId`. | ||
* **Returns:** `Promise` that resolves. | ||
|
||
* `WebSocketPlugin.close(instanceId, code, reason)` | ||
* same as `WebSocketInstance.close(code, reason)` but needs `instanceId`. | ||
* **Returns:** `Promise` that resolves. | ||
|
||
* `WebSocketInstance.send(message)` | ||
|
||
* Sends a message to the server. | ||
* Throws an error if the connection is not open. | ||
|
||
* `WebSocketInstance.close(code, reason)` | ||
|
||
* Closes the connection. | ||
* `code`: (Optional) If unspecified, a close code for the connection is automatically set: to 1000 for a normal closure, or otherwise to [another standard value in the range 1001-1015](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1) that indicates the actual reason the connection was closed. | ||
* `reason`: A string providing a [custom WebSocket connection close reason](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6) (a concise human-readable prose explanation for the closure). The value must be no longer than 123 bytes (encoded in UTF-8). | ||
|
||
--- | ||
|
||
### Properties of `WebSocketInstance` | ||
|
||
* `onopen`: Event listener for connection open. | ||
* `onmessage`: Event listener for messages received. | ||
* `onclose`: Event listener for connection close. | ||
* `onerror`: Event listener for errors. | ||
* `readyState`: (number) The state of the connection. | ||
|
||
* 0 (`CONNECTING`): Socket created, not yet open. | ||
* 1 (`OPEN`): Connection is open and ready. | ||
* 2 (`CLOSING`): Connection is closing. | ||
* 3 (`CLOSED`): Connection is closed or couldn't be opened. | ||
* `extensions`: (string) Extensions negotiated by the server (usually empty or a list). | ||
|
||
--- | ||
|
||
## Notes | ||
|
||
* Only supported on Android (via OkHttp). | ||
* Make sure to handle connection lifecycle properly (close sockets when done). | ||
* `listClients()` is useful for debugging and management. | ||
--- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "cordova-plugin-websocket", | ||
"version": "0.0.1", | ||
"description": "This cordova plugin is created to use WebSocket (client) in web/js.", | ||
"cordova": { | ||
"id": "cordova-plugin-websocket", | ||
"platforms": [ | ||
"android" | ||
] | ||
}, | ||
"keywords": [ | ||
"cordova", | ||
"websocket", | ||
"cordova-android", | ||
"ws" | ||
], | ||
"author": "Acode-Foundation (created by UnschooledGamer)", | ||
"license": "Apache-2.0", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<plugin id="cordova-plugin-websocket" version="0.0.1" xmlns="http://apache.org/cordova/ns/plugins/1.0"> | ||
<name>cordova-plugin-websocket</name> | ||
<description>Cordova Websocket</description> | ||
<license>MIT</license> | ||
<keywords>cordova,ws,WebSocket</keywords> | ||
<js-module src="www/websocket.js" name="WebSocket"> | ||
<clobbers target="cordova.websocket" /> | ||
</js-module> | ||
|
||
<platform name="android"> | ||
<config-file target="res/xml/config.xml" parent="/*"> | ||
<feature name="WebSocketPlugin"> | ||
<param name="android-package" value="com.foxdebug.websocket.WebSocketPlugin" /> | ||
</feature> | ||
</config-file> | ||
<source-file src="src/android/WebSocketPlugin.java" target-dir="src/com/foxdebug/websocket" /> | ||
<source-file src="src/android/WebSocketInstance.java" target-dir="src/com/foxdebug/websocket" /> | ||
<framework src="com.squareup.okhttp3:okhttp:4.12.0" /> | ||
</platform> | ||
</plugin> |
168 changes: 168 additions & 0 deletions
168
src/plugins/websocket/src/android/WebSocketInstance.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package com.foxdebug.websocket; | ||
|
||
import android.util.Log; | ||
|
||
import androidx.annotation.NonNull; | ||
|
||
import org.apache.cordova.*; | ||
import org.json.JSONArray; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
|
||
import java.util.Iterator; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import okhttp3.*; | ||
|
||
public class WebSocketInstance extends WebSocketListener { | ||
private static final int DEFAULT_CLOSE_CODE = 1000; | ||
private static final String DEFAULT_CLOSE_REASON = "Normal closure"; | ||
|
||
private WebSocket webSocket; | ||
private CallbackContext callbackContext; | ||
private final CordovaInterface cordova; | ||
private final String instanceId; | ||
private String extensions = ""; | ||
private int readyState = 0; // CONNECTING | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// okHttpMainClient parameter is used. To have a single main client(singleton), with per-websocket configuration using newBuilder method. | ||
public WebSocketInstance(String url, JSONArray protocols, JSONObject headers, OkHttpClient okHttpMainClient, CordovaInterface cordova, String instanceId) { | ||
this.cordova = cordova; | ||
this.instanceId = instanceId; | ||
|
||
OkHttpClient client = okHttpMainClient.newBuilder() | ||
.connectTimeout(10, TimeUnit.SECONDS) | ||
.build(); | ||
|
||
Request.Builder requestBuilder = new Request.Builder().url(url); | ||
|
||
// custom headers support. | ||
if (headers != null) { | ||
Iterator<String> keys = headers.keys(); | ||
while (keys.hasNext()) { | ||
String key = keys.next(); | ||
String value = headers.optString(key); | ||
requestBuilder.addHeader(key, value); | ||
} | ||
} | ||
|
||
// adds Sec-WebSocket-Protocol header if protocols is present. | ||
if (protocols != null) { | ||
StringBuilder protocolHeader = new StringBuilder(); | ||
for (int i = 0; i < protocols.length(); i++) { | ||
protocolHeader.append(protocols.optString(i)).append(","); | ||
} | ||
if (protocolHeader.length() > 0) { | ||
protocolHeader.setLength(protocolHeader.length() - 1); | ||
requestBuilder.addHeader("Sec-WebSocket-Protocol", protocolHeader.toString()); | ||
} | ||
} | ||
|
||
client.newWebSocket(requestBuilder.build(), this); | ||
} | ||
|
||
public void setCallback(CallbackContext callbackContext) { | ||
this.callbackContext = callbackContext; | ||
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); | ||
result.setKeepCallback(true); | ||
callbackContext.sendPluginResult(result); | ||
} | ||
|
||
public void send(String message) { | ||
if (this.webSocket != null) { | ||
this.webSocket.send(message); | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received send() action call, sending message=" + message); | ||
} else { | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received send() action call, ignoring... as webSocket is null (not present)"); | ||
} | ||
} | ||
|
||
public String close(int code, String reason) { | ||
if (this.webSocket != null) { | ||
this.readyState = 2; // CLOSING | ||
try { | ||
boolean result = this.webSocket.close(code, reason); | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call"); | ||
|
||
// if a graceful shutdown was already underway... | ||
// or if the web socket is already closed or canceled. do nothing. | ||
if(!result) { | ||
return null; | ||
} | ||
} catch (Exception e) { | ||
return e.getMessage(); | ||
} | ||
|
||
return null; | ||
} else { | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call, ignoring... as webSocket is null (not present)"); | ||
// TODO: finding a better way of telling it wasn't successful. | ||
return ""; | ||
} | ||
} | ||
|
||
public String close() { | ||
Log.d("WebSocketInstance", "WebSocket instanceId=" + this.instanceId + " close() called with no arguments. Using defaults."); | ||
// Calls the more specific version with default values | ||
return close(DEFAULT_CLOSE_CODE, DEFAULT_CLOSE_REASON); | ||
} | ||
|
||
@Override | ||
public void onOpen(@NonNull WebSocket webSocket, Response response) { | ||
this.webSocket = webSocket; | ||
this.readyState = 1; // OPEN | ||
this.extensions = response.headers("Sec-WebSocket-Extensions").toString(); | ||
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Opened" + "received extensions=" + this.extensions); | ||
sendEvent("open", null); | ||
} | ||
|
||
@Override | ||
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) { | ||
sendEvent("message", text); | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message: " + text); | ||
} | ||
|
||
@Override | ||
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) { | ||
this.readyState = 2; // CLOSING | ||
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason); | ||
} | ||
|
||
@Override | ||
public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) { | ||
this.readyState = 3; // CLOSED | ||
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Closed code: " + code + " reason: " + reason); | ||
JSONObject closedEvent = new JSONObject(); | ||
try { | ||
closedEvent.put("code", code); | ||
closedEvent.put("reason", reason); | ||
} catch (JSONException e) { | ||
Log.e("WebSocketInstance", "Error creating close event", e); | ||
} | ||
sendEvent("close", closedEvent.toString()); | ||
} | ||
|
||
@Override | ||
public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) { | ||
this.readyState = 3; // CLOSED | ||
sendEvent("error", t.getMessage()); | ||
Log.e("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage()); | ||
} | ||
|
||
private void sendEvent(String type, String data) { | ||
if (callbackContext != null) { | ||
try { | ||
JSONObject event = new JSONObject(); | ||
event.put("type", type); | ||
event.put("extensions", this.extensions); | ||
event.put("readyState", this.readyState); | ||
if (data != null) event.put("data", data); | ||
PluginResult result = new PluginResult(PluginResult.Status.OK, event); | ||
result.setKeepCallback(true); | ||
callbackContext.sendPluginResult(result); | ||
} catch (Exception e) { | ||
Log.e("WebSocketInstance", "Error sending event", e); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The license in plugin.xml is set to MIT, but package.json specifies Apache-2.0. Please align the license entries to avoid confusion for consumers.
Copilot uses AI. Check for mistakes.