-
Notifications
You must be signed in to change notification settings - Fork 511
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 2 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
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,85 @@ | ||
# 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 | ||
* ✅ Support for protocols | ||
* ✅ Compatible with Cordova for Android | ||
|
||
--- | ||
|
||
## Usage | ||
|
||
### Import | ||
|
||
```javascript | ||
const WebSocketPlugin = cordova.websocket; | ||
``` | ||
|
||
### Connect to WebSocket | ||
|
||
```javascript | ||
WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"]) | ||
.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)` | ||
|
||
* Connects to a WebSocket server. | ||
* `url`: The WebSocket server URL. | ||
* `protocols`: (Optional) An array of subprotocol strings. | ||
* Returns: A Promise that resolves to a `WebSocketInstance`. | ||
|
||
* `WebSocketInstance.send(message)` | ||
|
||
* Sends a message to the server. | ||
* Throws an error if the connection is not open. | ||
|
||
* `WebSocketInstance.close()` | ||
|
||
* Closes the connection. | ||
|
||
--- | ||
|
||
### 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). | ||
|
||
--- |
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> |
114 changes: 114 additions & 0 deletions
114
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,114 @@ | ||
package com.foxdebug.websocket; | ||
|
||
import android.util.Log; | ||
|
||
import androidx.annotation.NonNull; | ||
|
||
import org.apache.cordova.*; | ||
import org.json.JSONArray; | ||
import org.json.JSONObject; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
import okhttp3.*; | ||
|
||
public class WebSocketInstance extends WebSocketListener { | ||
private WebSocket webSocket; | ||
private CallbackContext callbackContext; | ||
private CordovaInterface cordova; | ||
private String instanceId; | ||
private String extensions = ""; | ||
private int readyState = 0; // CONNECTING | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public WebSocketInstance(String url, JSONArray protocols, CordovaInterface cordova, String instanceId) { | ||
this.cordova = cordova; | ||
this.instanceId = instanceId; | ||
|
||
OkHttpClient client = new OkHttpClient.Builder() | ||
.connectTimeout(10, TimeUnit.SECONDS) | ||
.build(); | ||
|
||
Request.Builder requestBuilder = new Request.Builder().url(url); | ||
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 (webSocket != null) { | ||
webSocket.send(message); | ||
} | ||
} | ||
|
||
public void close() { | ||
if (webSocket != null) { | ||
readyState = 2; // CLOSING | ||
webSocket.close(1000, "Normal closure"); | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call"); | ||
} | ||
|
||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " received close() action call, ignoring... as webSocket is null (not present)"); | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
@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, String text) { | ||
sendEvent("message", text); | ||
Log.d("WebSocketInstance", "websocket instanceId=" + this.instanceId + " Received message: " + text); | ||
} | ||
|
||
@Override | ||
public void onClosing(WebSocket webSocket, int code, String reason) { | ||
this.readyState = 2; // CLOSING | ||
sendEvent("close", reason); | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Log.i("WebSocketInstance", "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason); | ||
} | ||
|
||
@Override | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public void onFailure(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); | ||
} | ||
} | ||
} | ||
} |
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,63 @@ | ||
package com.foxdebug.websocket; | ||
|
||
import org.apache.cordova.*; | ||
import org.json.*; | ||
|
||
import java.util.HashMap; | ||
import java.util.UUID; | ||
|
||
// @TODO: plugin init & plugin destroy(closing okhttp clients) lifecycles. | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public class WebSocketPlugin extends CordovaPlugin { | ||
private static final HashMap<String, WebSocketInstance> instances = new HashMap<>(); | ||
UnschooledGamer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Override | ||
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { | ||
switch (action) { | ||
case "connect": | ||
String url = args.optString(0); | ||
JSONArray protocols = args.optJSONArray(1); | ||
String id = UUID.randomUUID().toString(); | ||
WebSocketInstance instance = new WebSocketInstance(url, protocols, cordova, id); | ||
instances.put(id, instance); | ||
callbackContext.success(id); | ||
return true; | ||
|
||
case "send": | ||
String instanceId = args.optString(0); | ||
String message = args.optString(1); | ||
WebSocketInstance inst = instances.get(instanceId); | ||
if (inst != null) { | ||
inst.send(message); | ||
callbackContext.success(); | ||
} else { | ||
callbackContext.error("Invalid instance ID"); | ||
} | ||
return true; | ||
|
||
case "close": | ||
instanceId = args.optString(0); | ||
inst = instances.get(instanceId); | ||
if (inst != null) { | ||
inst.close(); | ||
instances.remove(instanceId); | ||
callbackContext.success(); | ||
} else { | ||
callbackContext.error("Invalid instance ID"); | ||
} | ||
return true; | ||
|
||
case "registerListener": | ||
instanceId = args.optString(0); | ||
inst = instances.get(instanceId); | ||
if (inst != null) { | ||
inst.setCallback(callbackContext); | ||
} else { | ||
callbackContext.error("Invalid instance ID"); | ||
} | ||
return true; | ||
|
||
default: | ||
return false; | ||
} | ||
} | ||
} |
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.