com.google.cordova.ChromeSocket.java Source code

Java tutorial

Introduction

Here is the source code for com.google.cordova.ChromeSocket.java

Source

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package com.google.cordova;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.cordova.CordovaArgs;
import org.apache.cordova.api.CallbackContext;
import org.apache.cordova.api.CordovaPlugin;
import org.apache.cordova.api.PluginResult;
import org.json.JSONException;
import org.json.JSONObject;

import android.util.Log;

public class ChromeSocket extends CordovaPlugin {

    private static final String LOG_TAG = "ChromeSocket";

    private static Map<Integer, SocketData> sockets = new HashMap<Integer, SocketData>();
    private static int nextSocket = 1;

    @Override
    public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext)
            throws JSONException {
        if ("create".equals(action)) {
            create(args, callbackContext);
            return true;
        } else if ("connect".equals(action)) {
            connect(args, callbackContext);
            return true;
        } else if ("bind".equals(action)) {
            bind(args, callbackContext);
            return true;
        } else if ("write".equals(action)) {
            write(args, callbackContext);
            return true;
        } else if ("read".equals(action)) {
            read(args, callbackContext);
            return true;
        } else if ("sendTo".equals(action)) {
            sendTo(args, callbackContext);
            return true;
        } else if ("recvFrom".equals(action)) {
            recvFrom(args, callbackContext);
            return true;
        } else if ("disconnect".equals(action)) {
            disconnect(args, callbackContext);
            return true;
        } else if ("destroy".equals(action)) {
            destroy(args, callbackContext);
            return true;
        } else if ("listen".equals(action)) {
            listen(args, callbackContext);
            return true;
        } else if ("accept".equals(action)) {
            accept(args, callbackContext);
            return true;
        }
        return false;
    }

    private void create(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        String socketType = args.getString(0);
        if (socketType.equals("tcp") || socketType.equals("udp")) {
            SocketData sd = new SocketData(socketType.equals("tcp") ? SocketData.Type.TCP : SocketData.Type.UDP);
            int id = addSocket(sd);
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, id));
        } else {
            Log.e(LOG_TAG, "Unknown socket type: " + socketType);
        }
    }

    private static int addSocket(SocketData sd) {
        sockets.put(Integer.valueOf(nextSocket), sd);
        return nextSocket++;
    }

    private void connect(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);
        String address = args.getString(1);
        int port = args.getInt(2);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        boolean success = sd.connect(address, port);
        if (success)
            callbackContext.success();
        else
            callbackContext.error("Failed to connect");
    }

    private void bind(CordovaArgs args, final CallbackContext context) throws JSONException {
        int socketId = args.getInt(0);
        String address = args.getString(1);
        int port = args.getInt(2);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        boolean success = sd.bind(address, port);
        if (success)
            context.success();
        else
            context.error("Failed to bind.");
    }

    private void write(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);
        byte[] data = args.getArrayBuffer(1);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        int result = sd.write(data);
        if (result <= 0) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, result));
        } else {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
        }
    }

    private void read(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);
        int bufferSize = args.getInt(1);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        // Will call the callback once it has some data.
        sd.read(bufferSize, callbackContext);
    }

    private void sendTo(CordovaArgs args, final CallbackContext context) throws JSONException {
        JSONObject opts = args.getJSONObject(0);
        int socketId = opts.getInt("socketId");
        String address = opts.getString("address");
        int port = opts.getInt("port");
        byte[] data = args.getArrayBuffer(1);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        int result = sd.sendTo(data, address, port);
        if (result <= 0) {
            context.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, result));
        } else {
            context.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));
        }
    }

    private void recvFrom(CordovaArgs args, final CallbackContext context) throws JSONException {
        int socketId = args.getInt(0);
        int bufferSize = args.getInt(1);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        sd.recvFrom(bufferSize, context);
    }

    private void disconnect(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        sd.disconnect();
        callbackContext.success();
    }

    private void destroy(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        sd.destroy();
        sockets.remove(Integer.valueOf(socketId));
        callbackContext.success();
    }

    private void listen(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        String address = args.getString(1);
        int port = args.getInt(2);
        int backlog = args.getInt(3);
        boolean success = sd.listen(address, port, backlog);
        if (success)
            callbackContext.success();
        else
            callbackContext.error("Failed to listen()");
    }

    private void accept(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        int socketId = args.getInt(0);

        SocketData sd = sockets.get(Integer.valueOf(socketId));
        if (sd == null) {
            Log.e(LOG_TAG, "No socket with socketId " + socketId);
            return;
        }

        sd.accept(callbackContext);
    }

    private static class SocketData {
        Socket tcpSocket;
        DatagramSocket udpSocket;
        ServerSocket serverSocket;

        public enum Type {
            TCP, UDP;
        }

        private Type type;

        // Cached values used by UDP read()/write().
        private InetAddress address;
        private int port;
        private boolean connected = false; // Only applies to UDP, where connect() restricts who the socket will receive from.
        private boolean bound = false;

        private boolean isServer = false;

        BlockingQueue<ReadData> readQueue;
        private ReadThread readThread;

        BlockingQueue<AcceptData> acceptQueue;
        private AcceptThread acceptThread;

        public SocketData(Type type) {
            this.type = type;
        }

        public SocketData(Socket incoming) {
            this.type = Type.TCP;
            tcpSocket = incoming;
            init();
        }

        public boolean connect(String address, int port) {
            if (isServer)
                return false;
            try {
                if (type == Type.TCP) {
                    tcpSocket = new Socket(address, port);
                } else {
                    if (udpSocket == null) {
                        udpSocket = new DatagramSocket();
                    }
                    this.port = port;
                    this.address = InetAddress.getByName(address);
                    this.connected = true;
                    udpSocket.connect(this.address, port);
                }
                init();
            } catch (UnknownHostException uhe) {
                Log.e(LOG_TAG, "Unknown host exception while connecting socket", uhe);
                return false;
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "IOException while connecting socket", ioe);
                return false;
            }
            return true;
        }

        private void init() {
            readQueue = new LinkedBlockingQueue<ReadData>();
            readThread = new ReadThread();
            readThread.start();
        }

        public void udpInit() {
            try {
                udpSocket = new DatagramSocket();
            } catch (SocketException se) {
                Log.w(LOG_TAG, "SocketException while trying to create a UDP socket in sendTo()", se);
                return;
            }
            init();
        }

        public boolean bind(String address, int port) {
            if (type != Type.UDP) {
                Log.e(LOG_TAG, "bind() cannot be called on TCP sockets.");
                return false;
            }

            try {
                if (udpSocket == null) {
                    udpSocket = new DatagramSocket(port);
                    init();
                } else {
                    udpSocket.bind(new InetSocketAddress(port));
                }
                this.bound = true;
            } catch (SocketException se) {
                Log.e(LOG_TAG, "Failed to create UDP socket.", se);
                return false;
            }
            return true;
        }

        public int write(byte[] data) throws JSONException {
            if ((tcpSocket == null && udpSocket == null) || isServer)
                return -1;

            int bytesWritten = 0;
            try {
                if (type == Type.TCP) {
                    tcpSocket.getOutputStream().write(data);
                    bytesWritten = data.length;
                } else {
                    if (!connected) {
                        Log.e(LOG_TAG, "Cannot write() to unconnected UDP socket.");
                        return -1;
                    }

                    DatagramPacket packet = new DatagramPacket(data, data.length, this.address, this.port);
                    udpSocket.send(packet);
                    bytesWritten = data.length;
                }
            } catch (IOException ioe) {
                Log.w(LOG_TAG, "IOException while writing to socket", ioe);
                bytesWritten = -1;
            }

            return bytesWritten;
        }

        public int sendTo(byte[] data, String address, int port) {
            if (type != Type.UDP) {
                Log.w(LOG_TAG, "sendTo() can only be called for UDP sockets.");
                return -1;
            }

            // Create the socket and initialize the reading side, if connect() was never called.
            if (udpSocket == null) {
                udpInit();
            }

            int bytesWritten = 0;
            try {
                DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName(address), port);
                udpSocket.send(packet);
                bytesWritten = data.length;
            } catch (IOException ioe) {
                Log.w(LOG_TAG, "IOException in sendTo", ioe);
                bytesWritten = -1;
            }

            return bytesWritten;
        }

        public void read(int bufferLength, CallbackContext context) {
            if (isServer) {
                context.error("read() is not allowed on server sockets");
                return;
            }

            if (type == Type.UDP && !bound) {
                context.error("read() is not allowed on unbound UDP sockets.");
                return;
            }

            synchronized (readQueue) {
                try {
                    readQueue.put(new ReadData(bufferLength, context));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void recvFrom(int bufferSize, CallbackContext context) {
            if (type != Type.UDP) {
                context.error("recvFrom() is not allowed on non-UDP sockets");
                return;
            }

            // Create the socket and initialize the reading side, if connect() was never called.
            if (!bound) {
                context.error("Cannot recvFrom() without bind() first.");
                return;
            }

            synchronized (readQueue) {
                try {
                    // Flagged as recvFrom, therefore the two-part callback.
                    readQueue.put(new ReadData(bufferSize, context, true));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void disconnect() {
            try {
                if (isServer) {
                    acceptQueue.put(new AcceptData(true));
                    serverSocket.close();
                    // readQueue == null means that connect() failed.
                } else if (readQueue != null) {
                    readQueue.put(new ReadData(true));
                    if (type == Type.TCP) {
                        tcpSocket.close();
                    } else {
                        udpSocket.close();
                    }
                }
            } catch (IOException ioe) {
            } catch (InterruptedException ie) {
            }
            tcpSocket = null;
            udpSocket = null;
            serverSocket = null;
        }

        public void destroy() {
            if (tcpSocket != null || udpSocket != null || serverSocket != null) {
                disconnect();
            }
        }

        public boolean listen(String address, int port, int backlog) {
            if (type != Type.TCP)
                return false;
            isServer = true;

            try {
                serverSocket = new ServerSocket(port, backlog);
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "Error creating server socket", ioe);
                return false;
            }
            return true;
        }

        public void accept(CallbackContext context) {
            if (!isServer) {
                context.error("accept() is not supported on client sockets. Call listen() first.");
                return;
            }

            if (acceptQueue == null && acceptThread == null) {
                acceptQueue = new LinkedBlockingQueue<AcceptData>();
                acceptThread = new AcceptThread();
                acceptThread.start();
            }

            synchronized (acceptQueue) {
                try {
                    acceptQueue.put(new AcceptData(context));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private static class ReadData {
            public int size;
            public boolean killThread;
            public boolean recvFrom;
            public CallbackContext context;

            public ReadData(int size, CallbackContext context) {
                this.size = size;
                this.context = context;
                this.killThread = false;
                this.recvFrom = false;
            }

            public ReadData(int size, CallbackContext context, boolean recvFrom) {
                this.size = size;
                this.context = context;
                this.killThread = false;
                this.recvFrom = recvFrom;
            }

            public ReadData(boolean killThread) {
                this.killThread = true;
            }
        }

        private class ReadThread extends Thread {
            public void run() {
                try {
                    while (true) {
                        // Read from the blocking queue
                        ReadData readData = SocketData.this.readQueue.take();
                        if (readData.killThread)
                            return;

                        int toRead = readData.size;
                        byte[] out;
                        int bytesRead;

                        if (type == Type.TCP) {
                            if (toRead > 0) {
                                out = new byte[toRead];
                                bytesRead = SocketData.this.tcpSocket.getInputStream().read(out);
                            } else {
                                int firstByte = SocketData.this.tcpSocket.getInputStream().read();
                                out = new byte[SocketData.this.tcpSocket.getInputStream().available() + 1];
                                out[0] = (byte) firstByte;
                                bytesRead = SocketData.this.tcpSocket.getInputStream().read(out, 1, out.length - 1);
                                bytesRead++;
                            }

                            // Check for EOF
                            if (bytesRead < 0) {
                                SocketData.this.disconnect();
                                return;
                            }

                            readData.context.success(out);
                        } else {
                            if (toRead > 0) {
                                out = new byte[toRead];
                            } else {
                                out = new byte[4096]; // Defaults to 4K chunks.
                            }

                            DatagramPacket packet = new DatagramPacket(out, out.length);
                            udpSocket.receive(packet);

                            // Truncate the buffer if the message was shorter than it.
                            if (packet.getLength() != out.length) {
                                byte[] temp = new byte[packet.getLength()];
                                for (int i = 0; i < packet.getLength(); i++) {
                                    temp[i] = out[i];
                                }
                                out = temp;
                            }

                            PluginResult dataResult = new PluginResult(PluginResult.Status.OK, out);
                            // If this was a recvFrom() call, keep the callback for the two-part response.
                            // If this was a read() call, don't keep the callback.
                            dataResult.setKeepCallback(readData.recvFrom);
                            readData.context.sendPluginResult(dataResult);

                            if (readData.recvFrom) {
                                JSONObject obj = new JSONObject();
                                try {
                                    obj.put("address", packet.getAddress().getHostAddress());
                                    obj.put("port", packet.getPort());
                                } catch (JSONException je) {
                                    Log.e(LOG_TAG, "Error constructing JSON object to return from recvFrom()", je);
                                    return;
                                }
                                readData.context.success(obj);
                            }
                        }
                    } // while
                } catch (IOException ioe) {
                    Socket s = SocketData.this.tcpSocket;
                    if (s != null && s.isClosed()) {
                        Log.i(LOG_TAG, "Socket closed.");
                    } else {
                        Log.w(LOG_TAG, "Failed to read from socket.", ioe);
                    }
                } catch (InterruptedException ie) {
                    Log.w(LOG_TAG, "Thread interrupted", ie);
                }
            } // run()
        } // ReadThread

        private static class AcceptData {
            public boolean killThread;
            public CallbackContext context;

            public AcceptData(boolean killThread) {
                this.killThread = killThread;
            }

            public AcceptData(CallbackContext context) {
                this.context = context;
                this.killThread = false;
            }
        }

        private class AcceptThread extends Thread {
            public void run() {
                try {
                    while (true) {
                        AcceptData acceptData = SocketData.this.acceptQueue.take();
                        if (acceptData.killThread)
                            return;

                        Socket incoming = SocketData.this.serverSocket.accept();
                        if (SocketData.this.serverSocket == null || SocketData.this.serverSocket.isClosed()) {
                            if (incoming != null)
                                incoming.close();
                            return;
                        }

                        SocketData sd = new SocketData(incoming);
                        int id = ChromeSocket.addSocket(sd);
                        acceptData.context.sendPluginResult(new PluginResult(PluginResult.Status.OK, id));
                    }
                } catch (InterruptedException ie) {
                    Log.w(LOG_TAG, "Thread interrupted", ie);
                } catch (IOException ioe) {
                    if (SocketData.this.serverSocket == null || SocketData.this.serverSocket.isClosed()) {
                        Log.i(LOG_TAG, "Killing accept() thread; server socket closed.");
                    } else {
                        Log.w(LOG_TAG, "Error in accept() thread.", ioe);
                    }
                }
            } // run()
        } // AcceptThread
    } // SocketData
}