diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..a06dfcc --- /dev/null +++ b/plugin.xml @@ -0,0 +1,66 @@ + + + + SocketsForCordova + Cordova plugin for socket communication + Apache 2.0 + cordova,sockets,socket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/socket.js b/socket.js new file mode 100644 index 0000000..47513be --- /dev/null +++ b/socket.js @@ -0,0 +1,154 @@ +var exec = require('cordova/exec'); + +var SOCKET_EVENT = "SOCKET_EVENT"; +var CORDOVA_SERVICE_NAME = "SocketsForCordova"; + +function Socket() { + this.onData = null; + this.onClose = null; + this.onError = null; + this.socketKey = guid(); +} + +Socket.create = function(callback) { + + var socket = new Socket(); + + function socketEventHandler(event) { + + var payload = event.payload; + + if (payload.socketKey !== socket.socketKey) { + return; + } + + switch(payload.type) { + case "Close": + console.debug("SocketsForCordova: Close event, socket key: " + payload.socketKey); + window.document.removeEventListener(SOCKET_EVENT, socketEventHandler); + socket.onClose(); + break; + case "DataReceived": + console.debug("SocketsForCordova: DataReceived event, socket key: " + payload.socketKey); + socket.onData(new Int8Array(payload.data)); + break; + case "Error": + console.debug("SocketsForCordova: Error event, socket key: " + payload.socketKey); + socket.onError(payload.errorMessage); + break; + default: + console.error("SocketsForCordova: Unknown event type " + payload.type + ", socket key: " + payload.socketKey); + break; + } + } + + window.document.addEventListener(SOCKET_EVENT, socketEventHandler); + + exec( + function() { + console.debug("SocketsForCordova: Socket object successfully constructed."); + callback(socket); + }, + function(error) { + console.error("SocketsForCordova: Unexpected error during constructing Socket object. Error: " + error); + }, + CORDOVA_SERVICE_NAME, + "create", + [ socket.socketKey ]); +}; + +Socket.prototype.connect = function (host, port, success, error) { + exec( + function() { + console.debug("SocketsForCordova: Socket successfully connected."); + if (success) + success(); + }, + function(errorMessage) { + console.error("SocketsForCordova: Error during socket connecting. Error: " + errorMessage); + if (error) + error(errorMessage); + }, + CORDOVA_SERVICE_NAME, + "connect", + [ this.socketKey, host, port ]); +}; + +Socket.prototype.write = function (data, success, error) { + + var dataToWrite = data instanceof Int8Array + ? Socket._copyToArray(data) + : data; + + exec( + function() { + console.debug("SocketsForCordova: Data successfully written to socket. Number of bytes: " + data.length); + if (success) + success(); + }, + function(errorMessage) { + console.error("SocketsForCordova: Error during writing data to socket. Error: " + errorMessage); + if (error) + error(errorMessage); + }, + CORDOVA_SERVICE_NAME, + "write", + [ this.socketKey, dataToWrite ]); +}; + +Socket._copyToArray = function(array) { + var outputArray = new Array(array.length); + for (var i = 0; i < array.length; i++) { + outputArray[i] = array[i]; + } + return outputArray; +}; + +Socket.prototype.close = function () { + exec( + function() { + console.debug("SocketsForCordova: Close successfully closed."); + }, + function(errorMessage) { + console.error("SocketsForCordova: Error when call close on socket. Error: " + errorMessage); + }, + CORDOVA_SERVICE_NAME, + "close", + [ this.socketKey ]); +}; + +Socket.dispatchEvent = function(event) { + var eventReceive = document.createEvent('Events'); + eventReceive.initEvent(SOCKET_EVENT, true, true); + eventReceive.payload = event; + + document.dispatchEvent(eventReceive); +}; + +var guid = (function() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return function() { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; +})(); + +// Register event dispatcher for Windows Phone +if (navigator.userAgent.match(/iemobile/i)) { + window.document.addEventListener("deviceready", function() { + exec( + Socket.dispatchEvent, + function(errorMessage) { + console.error("SocketsForCordova: Cannot register WP event dispatcher, Error: " + errorMessage); + }, + CORDOVA_SERVICE_NAME, + "registerWPEventDispatcher", + [ ]); + }); +} + +module.exports = Socket; diff --git a/src/android/src/cz/blocshop/socketsforcordova/Consumer.java b/src/android/src/cz/blocshop/socketsforcordova/Consumer.java new file mode 100644 index 0000000..ee38fa4 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/Consumer.java @@ -0,0 +1,5 @@ +package cz.blocshop.socketsforcordova; + +public interface Consumer { + void accept(T t); +} \ No newline at end of file diff --git a/src/android/src/cz/blocshop/socketsforcordova/Logging.java b/src/android/src/cz/blocshop/socketsforcordova/Logging.java new file mode 100644 index 0000000..f807fe1 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/Logging.java @@ -0,0 +1,10 @@ +package cz.blocshop.socketsforcordova; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Logging { + public static void Error(String klass, String message, Throwable t) { + Logger.getLogger(klass).log(Level.SEVERE, message, t); + } +} diff --git a/src/android/src/cz/blocshop/socketsforcordova/SocketAdapter.java b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapter.java new file mode 100644 index 0000000..ee131d5 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapter.java @@ -0,0 +1,15 @@ +package cz.blocshop.socketsforcordova; + +import java.io.IOException; +import java.net.SocketException; + + +public interface SocketAdapter { + public void connect(String host, int port) throws IOException; + public void write(byte[] data) throws IOException; + public void close() throws IOException; + public void setOptions(SocketAdapterOptions options) throws SocketException; + public void setDataConsumer(Consumer dataConsumer); + public void setCloseEventHandler(Consumer closeEventHandler); + public void setErrorHandler(Consumer errorHandler); +} \ No newline at end of file diff --git a/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterImpl.java b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterImpl.java new file mode 100644 index 0000000..4a565a7 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterImpl.java @@ -0,0 +1,142 @@ +package cz.blocshop.socketsforcordova; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +public class SocketAdapterImpl implements SocketAdapter { + + private final int INPUT_STREAM_BUFFER_SIZE = 16 * 1024; + private final Socket socket; + + private Consumer dataConsumer; + private Consumer closeEventHandler; + private Consumer exceptionHandler; + + public SocketAdapterImpl() { + this.socket = new Socket(); + } + + @Override + public void connect(String host, int port) throws IOException { + this.socket.connect(new InetSocketAddress(host, port)); + this.submitReadTask(); + } + + @Override + public void write(byte[] data) throws IOException { + this.socket.getOutputStream().write(data); + } + + @Override + public void close() throws IOException { + if (!this.socket.isClosed()) { + this.socket.shutdownOutput(); + } + } + + @Override + public void setOptions(SocketAdapterOptions options) throws SocketException { + if (options.getKeepAlive() != null) { + this.socket.setKeepAlive(options.getKeepAlive()); + } + if (options.getOobInline() != null) { + this.socket.setOOBInline(options.getOobInline()); + } + if (options.getSoLinger() != null) { + this.socket.setSoLinger(true, options.getSoLinger()); + } + if (options.getSoTimeout() != null) { + this.socket.setSoTimeout(options.getSoTimeout()); + } + if (options.getReceiveBufferSize() != null) { + this.socket.setReceiveBufferSize(options.getReceiveBufferSize()); + } + if (options.getSendBufferSize() != null) { + this.socket.setSendBufferSize(options.getSendBufferSize()); + } + if (options.getTrafficClass() != null) { + this.socket.setTrafficClass(options.getTrafficClass()); + } + } + + @Override + public void setDataConsumer(Consumer dataConsumer) { + this.dataConsumer = dataConsumer; + } + + @Override + public void setCloseEventHandler(Consumer closeEventHandler) { + this.closeEventHandler = closeEventHandler; + } + + @Override + public void setErrorHandler(Consumer exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + private void submitReadTask() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(new Runnable() { + @Override + public void run() { + runRead(); + } + }); + } + + private void runRead() { + boolean hasError = false; + try { + runReadLoop(); + } catch (IOException e) { + Logging.Error(SocketAdapterImpl.class.getName(), "Error during reading of socket input stream", e); + hasError = true; + invokeExceptionHandler(e); + } finally { + try { + socket.close(); + } catch (IOException e) { + Logging.Error(SocketAdapterImpl.class.getName(), "Error during closing of socket", e); + } finally { + invokeCloseEventHandler(hasError); + } + } + } + + private void runReadLoop() throws IOException { + byte[] buffer = new byte[INPUT_STREAM_BUFFER_SIZE]; + int bytesRead = 0; + + while ((bytesRead = socket.getInputStream().read(buffer)) >= 0) { + byte[] data = buffer.length == bytesRead + ? buffer + : Arrays.copyOfRange(buffer, 0, bytesRead); + + this.invokeDataConsumer(data); + } + } + + private void invokeDataConsumer(byte[] data) { + if (this.dataConsumer != null) { + this.dataConsumer.accept(data); + } + } + + private void invokeCloseEventHandler(boolean hasError) { + if (this.closeEventHandler != null) { + this.closeEventHandler.accept(hasError); + } + } + + private void invokeExceptionHandler(IOException exception) { + if (this.exceptionHandler != null) { + this.exceptionHandler.accept(exception); + } + } +} diff --git a/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterOptions.java b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterOptions.java new file mode 100644 index 0000000..ec0d67f --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/SocketAdapterOptions.java @@ -0,0 +1,110 @@ +package cz.blocshop.socketsforcordova; + +public class SocketAdapterOptions { + + private Boolean keepAlive; + private Boolean oobInline; + private Integer soLinger; + private Integer soTimeout; + private Integer receiveBufferSize; + private Integer sendBufferSize; + private Integer trafficClass; + + /** + * @return the keepAlive + */ + public Boolean getKeepAlive() { + return keepAlive; + } + + /** + * @param keepAlive the keepAlive to set + */ + public void setKeepAlive(Boolean keepAlive) { + this.keepAlive = keepAlive; + } + + /** + * @return the oobInline + */ + public Boolean getOobInline() { + return oobInline; + } + + /** + * @param oobInline the oobInline to set + */ + public void setOobInline(Boolean oobInline) { + this.oobInline = oobInline; + } + + /** + * @return the soLinger + */ + public Integer getSoLinger() { + return soLinger; + } + + /** + * @param soLinger the soLinger to set + */ + public void setSoLinger(Integer soLinger) { + this.soLinger = soLinger; + } + + /** + * @return the soTimeout + */ + public Integer getSoTimeout() { + return soTimeout; + } + + /** + * @param soTimeout the soTimeout to set + */ + public void setSoTimeout(Integer soTimeout) { + this.soTimeout = soTimeout; + } + + /** + * @return the receiveBufferSize + */ + public Integer getReceiveBufferSize() { + return receiveBufferSize; + } + + /** + * @param receiveBufferSize the receiveBufferSize to set + */ + public void setReceiveBufferSize(Integer receiveBufferSize) { + this.receiveBufferSize = receiveBufferSize; + } + + /** + * @return the sendBufferSize + */ + public Integer getSendBufferSize() { + return sendBufferSize; + } + + /** + * @param sendBufferSize the sendBufferSize to set + */ + public void setSendBufferSize(Integer sendBufferSize) { + this.sendBufferSize = sendBufferSize; + } + + /** + * @return the trafficClass + */ + public Integer getTrafficClass() { + return trafficClass; + } + + /** + * @param trafficClass the trafficClass to set + */ + public void setTrafficClass(Integer trafficClass) { + this.trafficClass = trafficClass; + } +} diff --git a/src/android/src/cz/blocshop/socketsforcordova/SocketPlugin.java b/src/android/src/cz/blocshop/socketsforcordova/SocketPlugin.java new file mode 100644 index 0000000..9f8f531 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/SocketPlugin.java @@ -0,0 +1,206 @@ +package cz.blocshop.socketsforcordova; + +import android.annotation.SuppressLint; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaArgs; +import org.apache.cordova.CordovaPlugin; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class SocketPlugin extends CordovaPlugin { + + Map socketAdapters = new HashMap(); + + @Override + public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException { + + if(action.equals("create")) { + this.create(args, callbackContext); + } else if (action.equals("connect")) { + this.connect(args, callbackContext); + } else if (action.equals("write")) { + this.write(args, callbackContext); + } else if (action.equals("close")) { + this.close(args, callbackContext); + } else if (action.equals("setOptions")) { + this.setOptions(args, callbackContext); + } else { + callbackContext.error(String.format("SocketPlugin - invalid action:", action)); + return false; + } + return true; + } + + private void create(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + + final String socketKey = args.getString(0); + + SocketAdapter socketAdapter = new SocketAdapterImpl(); + socketAdapter.setCloseEventHandler(new CloseEventHandler(socketKey)); + socketAdapter.setDataConsumer(new DataConsumer(socketKey)); + socketAdapter.setErrorHandler(new ErrorHandler(socketKey)); + + this.socketAdapters.put(socketKey, socketAdapter); + + callbackContext.success(socketKey); + } + + private void connect(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + String socketKey = args.getString(0); + String host = args.getString(1); + int port = args.getInt(2); + + SocketAdapter socket = this.getSocketAdapter(socketKey); + + try { + socket.connect(host, port); + callbackContext.success(); + } catch (IOException e) { + callbackContext.error(e.toString()); + } + } + + private void write(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + String socketKey = args.getString(0); + JSONArray data = args.getJSONArray(1); + + byte[] dataBuffer = new byte[data.length()]; + for(int i = 0; i < dataBuffer.length; i++) { + dataBuffer[i] = (byte) data.getInt(i); + } + + SocketAdapter socket = this.getSocketAdapter(socketKey); + + try { + socket.write(dataBuffer); + callbackContext.success(); + } catch (IOException e) { + callbackContext.error(e.toString()); + } + } + + private void close(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + String socketKey = args.getString(0); + + SocketAdapter socket = this.getSocketAdapter(socketKey); + + try { + socket.close(); + callbackContext.success(); + } catch (IOException e) { + callbackContext.error(e.toString()); + } + } + + private void setOptions(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + + String socketKey = args.getString(0); + JSONObject optionsJSON = args.getJSONObject(1); + + SocketAdapter socket = this.getSocketAdapter(socketKey); + + SocketAdapterOptions options = new SocketAdapterOptions(); + options.setKeepAlive(getBooleanPropertyFromJSON(optionsJSON, "keepAlive")); + options.setOobInline(getBooleanPropertyFromJSON(optionsJSON, "oobInline")); + options.setReceiveBufferSize(getIntegerPropertyFromJSON(optionsJSON, "receiveBufferSize")); + options.setSendBufferSize(getIntegerPropertyFromJSON(optionsJSON, "sendBufferSize")); + options.setSoLinger(getIntegerPropertyFromJSON(optionsJSON, "soLinger")); + options.setSoTimeout(getIntegerPropertyFromJSON(optionsJSON, "soTimeout")); + options.setTrafficClass(getIntegerPropertyFromJSON(optionsJSON, "trafficClass")); + + try { + socket.close(); + callbackContext.success(); + } catch (IOException e) { + callbackContext.error(e.toString()); + } + } + + private Boolean getBooleanPropertyFromJSON(JSONObject jsonObject, String propertyName) throws JSONException { + return jsonObject.has(propertyName) ? jsonObject.getBoolean(propertyName) : null; + } + + private Integer getIntegerPropertyFromJSON(JSONObject jsonObject, String propertyName) throws JSONException { + return jsonObject.has(propertyName) ? jsonObject.getInt(propertyName) : null; + } + + private SocketAdapter getSocketAdapter(String socketKey) { + if (!this.socketAdapters.containsKey(socketKey)) { + throw new IllegalArgumentException( + String.format("Cannot find socketKey: %s. Connection is probably closed.", socketKey)); + } + return this.socketAdapters.get(socketKey); + } + + private void dispatchEvent(JSONObject jsonEventObject) { + this.webView.sendJavascript(String.format("window.Socket.dispatchEvent(%s);", jsonEventObject.toString())); + } + + private class CloseEventHandler implements Consumer { + private String socketKey; + public CloseEventHandler(String socketKey) { + this.socketKey = socketKey; + } + @Override + public void accept(Boolean hasError) { + socketAdapters.remove(this.socketKey); + + try { + JSONObject event = new JSONObject(); + event.put("type", "Close"); + event.put("hasError", hasError.booleanValue()); + event.put("socketKey", this.socketKey); + + dispatchEvent(event); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + private class DataConsumer implements Consumer { + private String socketKey; + public DataConsumer(String socketKey) { + this.socketKey = socketKey; + } + @SuppressLint("NewApi") + @Override + public void accept(byte[] data) { + try { + JSONObject event = new JSONObject(); + event.put("type", "DataReceived"); + event.put("data", new JSONArray(data)); + event.put("socketKey", socketKey); + + dispatchEvent(event); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + private class ErrorHandler implements Consumer { + private String socketKey; + public ErrorHandler(String socketKey) { + this.socketKey = socketKey; + } + @Override + public void accept(IOException exception) { + try { + JSONObject event = new JSONObject(); + event.put("type", "Error"); + event.put("errorMessage", exception.toString()); + event.put("socketKey", socketKey); + + dispatchEvent(event); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/android/src/cz/blocshop/socketsforcordova/SocketsForCordovaActivity.java b/src/android/src/cz/blocshop/socketsforcordova/SocketsForCordovaActivity.java new file mode 100644 index 0000000..9d6e6a6 --- /dev/null +++ b/src/android/src/cz/blocshop/socketsforcordova/SocketsForCordovaActivity.java @@ -0,0 +1,37 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package cz.blocshop.socketsforcordova; + +import android.os.Bundle; +import org.apache.cordova.*; + +public class SocketsForCordovaActivity extends CordovaActivity +{ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + super.init(); + // Set by in config.xml + super.loadUrl(Config.getStartUrl()); + //super.loadUrl("file:///android_asset/www/index.html"); + } +} + diff --git a/src/wp8/src/SocketAdapter.cs b/src/wp8/src/SocketAdapter.cs new file mode 100644 index 0000000..83892ea --- /dev/null +++ b/src/wp8/src/SocketAdapter.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Blocshop.ScoketsForCordova +{ + public interface ISocketAdapter + { + Task Connect(String host, int port); + Task Write(byte[] data); + void Close(); + SocketAdapterOptions Options { set; } + Action DataConsumer { set; } + Action CloseEventHandler { set; } + Action ErrorHandler { set; } + } + + + public class SocketAdapter : ISocketAdapter + { + private const int InputStreamBufferSize = 16 * 1024; + private readonly Socket socket; + + public Action DataConsumer { get; set; } + public Action CloseEventHandler { get; set; } + public Action ErrorHandler { get; set; } + public SocketAdapterOptions Options { get; set; } + + public SocketAdapter() + { + this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + } + + public async Task Connect(string host, int port) + { + var connectSocketAsyncEventArgs = new SocketAsyncEventArgs + { + RemoteEndPoint = new DnsEndPoint(host, port) + }; + + await this.socket.ConnectTaskAsync(connectSocketAsyncEventArgs); + + this.StartReadTask(); + } + + public async Task Write(byte[] data) + { + var socketAsyncEventArgs = new SocketAsyncEventArgs(); + socketAsyncEventArgs.SetBuffer(data, 0, data.Length); + + await this.socket.SendTaskAsync(socketAsyncEventArgs); + } + + public void Close() + { + this.socket.Shutdown(SocketShutdown.Send); + } + + private void StartReadTask() + { + Task.Factory.StartNew(() => this.RunRead()); + } + + private async Task RunRead() + { + bool hasError = false; + try + { + await this.RunReadLoop(); + } + catch (SocketException ex) + { + hasError = true; + this.ErrorHandler(ex); + } + catch (Exception ex) + { + } + finally + { + this.socket.Close(); + this.CloseEventHandler(hasError); + } + } + + private async Task RunReadLoop() + { + byte[] buffer = new byte[InputStreamBufferSize]; + int bytesRead = 0; + do + { + var eventArgs = new SocketAsyncEventArgs(); + eventArgs.SetBuffer(buffer, 0, InputStreamBufferSize); + + await this.socket.ReceiveTaskAsync(eventArgs); + + bytesRead = eventArgs.BytesTransferred; + + byte[] data = new byte[bytesRead]; + Array.Copy(buffer, data, data.Length); + this.DataConsumer(data); + } + while (bytesRead != 0); + } + } +} diff --git a/src/wp8/src/SocketAdapterOptions.cs b/src/wp8/src/SocketAdapterOptions.cs new file mode 100644 index 0000000..dc09cee --- /dev/null +++ b/src/wp8/src/SocketAdapterOptions.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Blocshop.ScoketsForCordova +{ + public class SocketAdapterOptions + { + } +} diff --git a/src/wp8/src/SocketEvent.cs b/src/wp8/src/SocketEvent.cs new file mode 100644 index 0000000..75be9f1 --- /dev/null +++ b/src/wp8/src/SocketEvent.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Blocshop.ScoketsForCordova +{ + [DataContract] + public abstract class SocketEvent + { + [DataMember(Name = "type")] + public abstract string Type { get; set; } + + [DataMember(Name = "socketKey")] + public string SocketKey { get; set; } + } + + [DataContract] + public class CloseSocketEvent : SocketEvent + { + public override string Type + { + get + { + return "Close"; + } + set + { + } + } + + [DataMember(Name = "hasError")] + public bool HasError { get; set; } + } + + [DataContract] + public class DataReceivedSocketEvent : SocketEvent + { + public override string Type + { + get + { + return "DataReceived"; + } + set + { + } + } + + [DataMember(Name = "data")] + public byte[] Data { get; set; } + } + + [DataContract] + public class ErrorSocketEvent : SocketEvent + { + public override string Type + { + get + { + return "Error"; + } + set + { + } + } + + [DataMember(Name="errorMessage")] + public string ErrorMessage { get; set; } + } +} diff --git a/src/wp8/src/SocketExtensions.cs b/src/wp8/src/SocketExtensions.cs new file mode 100644 index 0000000..a8e2585 --- /dev/null +++ b/src/wp8/src/SocketExtensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Blocshop.ScoketsForCordova +{ + public static class SocketExtensions + { + public static async Task ConnectTaskAsync(this Socket socket, SocketAsyncEventArgs socketAsyncEventArgs) + { + var task = CreateTaskFromCompletionHandler(socketAsyncEventArgs, SocketAsyncOperation.Connect); + + socket.ConnectAsync(socketAsyncEventArgs); + + await task; + } + + public static async Task SendTaskAsync(this Socket socket, SocketAsyncEventArgs socketAsyncEventArgs) + { + var task = CreateTaskFromCompletionHandler(socketAsyncEventArgs, SocketAsyncOperation.Send); + + socket.SendAsync(socketAsyncEventArgs); + + await task; + } + + public static async Task ReceiveTaskAsync(this Socket socket, SocketAsyncEventArgs socketAsyncEventArgs) + { + var task = CreateTaskFromCompletionHandler(socketAsyncEventArgs, SocketAsyncOperation.Receive); + + socket.ReceiveAsync(socketAsyncEventArgs); + + await task; + + return socketAsyncEventArgs.Buffer; + } + + private static Task CreateTaskFromCompletionHandler(SocketAsyncEventArgs socketAsyncEventArgs, SocketAsyncOperation socketAsyncOperation) + { + TaskCompletionSource completionSource = new TaskCompletionSource(); + socketAsyncEventArgs.Completed += new EventHandler((o, eventArgs) => + { + if (eventArgs.LastOperation == socketAsyncOperation) + { + if (eventArgs.SocketError == SocketError.Success) + { + completionSource.SetResult(""); + } + else + { + completionSource.SetException(new SocketException((int)eventArgs.SocketError)); + } + } + }); + + return completionSource.Task; + } + } +} diff --git a/src/wp8/src/SocketPlugin.cs b/src/wp8/src/SocketPlugin.cs new file mode 100644 index 0000000..243eeb8 --- /dev/null +++ b/src/wp8/src/SocketPlugin.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using WPCordovaClassLib.Cordova; +using WPCordovaClassLib.Cordova.Commands; +using WPCordovaClassLib.Cordova.JSON; + +namespace Blocshop.ScoketsForCordova +{ + public class SocketPlugin : BaseCommand + { + private readonly ISocketStorage socketStorage; + private string eventDispatcherCallbackId; + + public SocketPlugin() + { + System.Diagnostics.Debug.WriteLine("SocketPlugin constructor: " + DateTime.Now.Ticks); + this.socketStorage = SocketStorage.CreateSocketStorage(); + } + + public void create(string parameters) + { + string socketKey = JsonHelper.Deserialize(parameters)[0]; + + ISocketAdapter socketAdapter = new SocketAdapter(); + socketAdapter.CloseEventHandler = (hasError) => this.CloseEventHandler(socketKey, hasError); + socketAdapter.DataConsumer = (data) => this.DataConsumer(socketKey, data); + socketAdapter.ErrorHandler = (ex) => this.ErrorHandler(socketKey, ex); + + this.socketStorage.Add(socketKey, socketAdapter); + + this.DispatchCommandResult(new PluginResult(PluginResult.Status.OK)); + } + + public void registerWPEventDispatcher(string parameters) + { + this.eventDispatcherCallbackId = this.CurrentCommandCallbackId; + PluginResult result = new PluginResult(PluginResult.Status.OK); + result.KeepCallback = true; + DispatchCommandResult(result, this.eventDispatcherCallbackId); + } + + public void connect(string parameters) + { + string socketKey = JsonHelper.Deserialize(parameters)[0]; + string host = JsonHelper.Deserialize(parameters)[1]; + int port = int.Parse(JsonHelper.Deserialize(parameters)[2]); + + ISocketAdapter socket = this.socketStorage.Get(socketKey); + try + { + socket.Connect(host, port).Wait(); + + this.DispatchCommandResult(new PluginResult(PluginResult.Status.OK)); + } + catch (SocketException ex) + { + this.DispatchCommandResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, ex.Message)); + } + } + + public void write(string parameters/*, string socketKey, byte[] data*/) + { + string socketKey = JsonHelper.Deserialize(parameters)[0]; + string dataJsonArray = JsonHelper.Deserialize(parameters)[1]; + byte[] data = JsonHelper.Deserialize(dataJsonArray); + + ISocketAdapter socket = this.socketStorage.Get(socketKey); + try + { + socket.Write(data).Wait(); + + this.DispatchCommandResult(new PluginResult(PluginResult.Status.OK)); + } + catch (SocketException ex) + { + this.DispatchCommandResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, ex.Message)); + } + } + + public void close(string parameters) + { + string socketKey = JsonHelper.Deserialize(parameters)[0]; + + ISocketAdapter socket = this.socketStorage.Get(socketKey); + + socket.Close(); + } + + //private void setOptions(CordovaArgs args, CallbackContext callbackContext) throws JSONException { + + // String socketKey = args.getString(0); + // JSONObject optionsJSON = args.getJSONObject(1); + + // SocketAdapter socket = this.getSkocketAdapter(socketKey); + + // SocketAdapterOptions options = new SocketAdapterOptions(); + // options.setKeepAlive(getBooleanPropertyFromJSON(optionsJSON, "keepAlive")); + // options.setOobInline(getBooleanPropertyFromJSON(optionsJSON, "oobInline")); + // options.setReceiveBufferSize(getIntegerPropertyFromJSON(optionsJSON, "receiveBufferSize")); + // options.setSendBufferSize(getIntegerPropertyFromJSON(optionsJSON, "sendBufferSize")); + // options.setSoLinger(getIntegerPropertyFromJSON(optionsJSON, "soLinger")); + // options.setSoTimeout(getIntegerPropertyFromJSON(optionsJSON, "soTimeout")); + // options.setTrafficClass(getIntegerPropertyFromJSON(optionsJSON, "trafficClass")); + + // try { + // socket.close(); + // callbackContext.success(); + // } catch (IOException e) { + // callbackContext.error(e.toString()); + // } + //} + + private void CloseEventHandler(string socketKey, bool hasError) + { + socketStorage.Remove(socketKey); + this.DispatchEvent(new CloseSocketEvent + { + HasError = hasError, + SocketKey = socketKey + }); + } + + private void DataConsumer(string socketKey, byte[] data) + { + this.DispatchEvent(new DataReceivedSocketEvent + { + Data = data, + SocketKey = socketKey + }); + } + + private void ErrorHandler(string socketKey, Exception exception) + { + this.DispatchEvent(new ErrorSocketEvent + { + ErrorMessage = exception.Message, + SocketKey = socketKey + }); + } + + private void DispatchEvent(SocketEvent eventObject) + { + PluginResult result = new PluginResult(PluginResult.Status.OK, JsonHelper.Serialize(eventObject)); + result.KeepCallback = true; + DispatchCommandResult(result, this.eventDispatcherCallbackId); + } + } +} diff --git a/src/wp8/src/SocketStorage.cs b/src/wp8/src/SocketStorage.cs new file mode 100644 index 0000000..3d77e78 --- /dev/null +++ b/src/wp8/src/SocketStorage.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blocshop.ScoketsForCordova +{ + public interface ISocketStorage + { + void Add(string socketKey, ISocketAdapter socketAdapter); + ISocketAdapter Get(string socketKey); + void Remove(string socketKey); + } + + public class SocketStorage : ISocketStorage + { + private readonly IDictionary socketAdapters = new Dictionary(); + + private object syncRoot = new object(); + + public void Add(string socketKey, ISocketAdapter socketAdapter) + { + lock (syncRoot) + { + System.Diagnostics.Debug.WriteLine("Add: " + DateTime.Now.Ticks); + this.socketAdapters.Add(socketKey, socketAdapter); + } + } + + public ISocketAdapter Get(string socketKey) + { + lock (syncRoot) + { + System.Diagnostics.Debug.WriteLine("Get: " + DateTime.Now.Ticks); + if (!this.socketAdapters.ContainsKey(socketKey)) + { + throw new ArgumentException( + string.Format("Cannot find socketKey: {0}. Connection is probably closed.", socketKey)); + } + + return this.socketAdapters[socketKey]; + } + } + + public void Remove(string socketKey) + { + lock (syncRoot) + { + System.Diagnostics.Debug.WriteLine("Remove: " + DateTime.Now.Ticks); + this.socketAdapters.Remove(socketKey); + } + } + + public static ISocketStorage CreateSocketStorage() + { + return new SocketStorage(); + } + } +}