-
Notifications
You must be signed in to change notification settings - Fork 512
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
Open
UnschooledGamer
wants to merge
12
commits into
Acode-Foundation:main
Choose a base branch
from
UnschooledGamer:feat/native-websocket-plugin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 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 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
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,6 @@ | ||
{ | ||
"image": "mcr.microsoft.com/devcontainers/universal:2", | ||
"features": { | ||
"ghcr.io/nordcominc/devcontainer-features/android-sdk:1": {} | ||
} | ||
} |
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> |
194 changes: 194 additions & 0 deletions
194
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,194 @@ | ||
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.*; | ||
|
||
import okio.ByteString; | ||
|
||
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 String protocol = ""; | ||
private String binaryType = ""; | ||
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, String binaryType, OkHttpClient okHttpMainClient, CordovaInterface cordova, String instanceId) { | ||
this.cordova = cordova; | ||
this.instanceId = instanceId; | ||
this.binaryType = binaryType; | ||
|
||
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(); | ||
this.protocol = response.header("Sec-WebSocket-Protocol"); | ||
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Opened" + "received extensions=" + this.extensions); | ||
sendEvent("open", null, false); | ||
} | ||
|
||
@Override | ||
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) { | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message: " + text); | ||
sendEvent("message", text, false); | ||
} | ||
|
||
// This is called when the Websocket server sends a binary(type 0x2) message. | ||
@Override | ||
public void onMessage(@NonNull WebSocket webSocket, @NonNull ByteString bytes) { | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message(bytes): " + bytes.toString()); | ||
|
||
try { | ||
if ("arraybuffer".equals(this.binaryType)) { | ||
String base64 = bytes.base64(); | ||
sendEvent("message", base64, true); | ||
} else { | ||
sendEvent("message", bytes.utf8(), true); | ||
} | ||
} catch (Exception e) { | ||
Log.e("WebSocketInstance", "Error sending message", e); | ||
} | ||
|
||
} | ||
|
||
@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(), false); | ||
} | ||
|
||
@Override | ||
public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) { | ||
this.readyState = 3; // CLOSED | ||
sendEvent("error", t.getMessage(), false); | ||
Log.e("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage()); | ||
} | ||
|
||
private void sendEvent(String type, String data, boolean isBinary) { | ||
if (callbackContext != null) { | ||
try { | ||
JSONObject event = new JSONObject(); | ||
event.put("type", type); | ||
event.put("extensions", this.extensions); | ||
event.put("readyState", this.readyState); | ||
event.put("isBinary", isBinary ? true : false); | ||
if (data != null) event.put("data", data); | ||
Log.d("WebSocketInstance", "sending event: " + type + " eventObj " + event.toString()); | ||
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.