org.chromium.ChromeSocket.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.ChromeSocket.java

Source

// Copyright (c) 2013 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 org.chromium;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

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

import android.util.Log;
import android.os.AsyncTask;

public class ChromeSocket extends CordovaPlugin {

    private static final String LOG_TAG = "ChromeSocket";

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

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

    public void onDestroy() {
        destroyAllSockets();
    }

    public void onReset() {
        destroyAllSockets();
    }

    private void destroyAllSockets() {
        if (sockets.isEmpty())
            return;

        Log.i(LOG_TAG, "Destroying all open sockets");

        for (Map.Entry<Integer, SocketData> entry : sockets.entrySet()) {
            SocketData sd = entry.getValue();
            sd.destroy();
        }
        sockets.clear();
    }

    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") ? SocketType.TCP : SocketType.UDP);
            int id = addSocket(sd);
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, id));
        } else {
            Log.e(LOG_TAG, "Unknown socket type: " + socketType);
        }
    }

    private 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;
        }

        // The SocketData.connect() method will callback appropriately
        sd.connect(address, port, callbackContext);
    }

    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;
        }

        new SendToTask().execute(sd, data, address, port, context);
    }

    private class SendToTask extends AsyncTask<Object, Void, Void> {
        @Override
        protected Void doInBackground(Object... argsArray) {
            SocketData sd = (SocketData) argsArray[0];
            byte[] data = (byte[]) argsArray[1];
            String address = (String) argsArray[2];
            int port = ((Integer) argsArray[3]).intValue();
            CallbackContext context = (CallbackContext) argsArray[4];

            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));
            }

            return null;
        }
    }

    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 void getInfo(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;
        }

        JSONObject info = sd.getInfo();
        callbackContext.success(info);
    }

    private void getNetworkList(CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        try {
            JSONArray list = new JSONArray();
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            NetworkInterface iface;
            // Enumerations are a crappy legacy API, can't use the for (foo : bar) syntax.
            while (interfaces.hasMoreElements()) {
                iface = interfaces.nextElement();
                if (iface.isLoopback()) {
                    continue;
                }
                for (InterfaceAddress interfaceAddress : iface.getInterfaceAddresses()) {
                    InetAddress address = interfaceAddress.getAddress();
                    if (address != null) {
                        JSONObject data = new JSONObject();
                        data.put("name", iface.getDisplayName());
                        // Strip percent suffix off of ipv6 addresses to match desktop behaviour.
                        data.put("address", address.getHostAddress().replaceAll("%.*", ""));
                        data.put("prefixLength", interfaceAddress.getNetworkPrefixLength());
                        list.put(data);
                    }
                }
            }

            callbackContext.success(list);
        } catch (SocketException se) {
            callbackContext.error("SocketException: " + se);
        }
    }

    // Multicast calls
    private void joinGroup(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 ret = sd.joinGroup(address);
        callbackContext.success(ret);
    }

    private void leaveGroup(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 ret = sd.leaveGroup(address);
        callbackContext.success(ret);
    }

    private void setMulticastTimeToLive(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;
        }

        int ttl = args.getInt(1);
        int ret = sd.setMulticastTimeToLive(ttl);
        callbackContext.success(ret);
    }

    private void setMulticastLoopbackMode(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;
        }

        boolean enabled = args.getBoolean(1);
        int ret = sd.setMulticastLoopbackMode(enabled);
        callbackContext.success(ret);
    }

    private void getJoinedGroups(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;
        }

        Collection<String> ret = sd.getJoinedGroups();
        callbackContext.success(new JSONArray(ret));
    }

    private enum SocketType {
        TCP, UDP;
    }

    private class SocketData {
        Socket tcpSocket;
        DatagramSocket udpSocket;
        MulticastSocket multicastSocket;
        ServerSocket serverSocket;

        private SocketType type;

        // Cached values used by UDP read()/write().
        // These are the REMOTE address and port.
        private InetAddress address;
        private int port;

        private int localPort;

        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;

        private boolean multicast = false;

        private ConnectThread connectThread;

        BlockingQueue<ReadData> readQueue;
        private ReadThread readThread;

        BlockingQueue<AcceptData> acceptQueue;
        private AcceptThread acceptThread;

        private HashSet<String> multicastGroups;

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

        public SocketData(Socket incoming) {
            this.type = SocketType.TCP;
            tcpSocket = incoming;
            connected = true;
            address = incoming.getInetAddress();
            port = incoming.getPort();
            init();
        }

        public JSONObject getInfo() throws JSONException {
            JSONObject info = new JSONObject();
            info.put("socketType", type == SocketType.TCP ? "tcp" : "udp");

            // According to the chrome.socket docs, this is always true for TCP sockets post-connect calls,
            // and for UDP it's true iff the connect() call has been used to set default remotes.
            // That's exactly what the boolean connected tracks.
            info.put("connected", connected);

            if (connected || isServer || bound) {
                if (connected) {
                    info.put("peerAddress", address.getHostAddress());
                    info.put("peerPort", port);
                }

                if (isServer) { // TCP server socket
                    info.put("localAddress", serverSocket.getInetAddress().getHostAddress());
                    info.put("localPort", serverSocket.getLocalPort());
                } else if (type == SocketType.TCP) {
                    info.put("localAddress", tcpSocket.getLocalAddress().getHostAddress());
                    info.put("localPort", tcpSocket.getLocalPort());
                } else { // UDP socket
                    info.put("localAddress", udpSocket.getLocalAddress().getHostAddress());
                    info.put("localPort", udpSocket.getLocalPort());
                }
            }

            return info;
        }

        public boolean connect(String address, int port, CallbackContext callbackContext) {
            if (isServer)
                return false;
            try {
                if (type == SocketType.TCP) {
                    connected = true;
                    this.address = InetAddress.getByName(address);
                    this.port = port;

                    this.connectThread = new ConnectThread(this.address, port, callbackContext);
                    this.connectThread.start();
                } else {
                    if (udpSocket == null) {
                        udpSocket = new DatagramSocket();
                    }
                    this.port = port;
                    this.address = InetAddress.getByName(address);
                    this.connected = true;
                    udpSocket.connect(this.address, port);
                    init();

                    callbackContext.success();
                }
            } catch (UnknownHostException uhe) {
                Log.e(LOG_TAG, "Unknown host exception while connecting socket", uhe);
                callbackContext.error("Unknown Host");
                return false;
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "IOException while connecting socket", ioe);
                callbackContext.error("IOException");
                return false;
            }
            return true;
        }

        public 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 != SocketType.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;
                this.localPort = port;
            } 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 == SocketType.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 != SocketType.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 == SocketType.UDP && !bound && !connected) {
                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 != SocketType.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 is null when listen has been called but not accept().
                    if (acceptQueue != null) {
                        acceptQueue.put(new AcceptData(true));
                    }
                    serverSocket.close();
                    // readQueue == null means that connect() failed.
                } else if (multicastSocket != null) {
                    for (String address : multicastGroups) {
                        multicastSocket.leaveGroup(InetAddress.getByName(address));
                    }
                    multicastSocket.close();
                    multicastGroups = null;
                    if (readQueue != null) {
                        readQueue.put(new ReadData(true));
                    }
                } else if (readQueue != null) {
                    readQueue.put(new ReadData(true));
                    if (type == SocketType.TCP) {
                        tcpSocket.close();
                    } else {
                        udpSocket.close();
                    }
                }
            } catch (IOException ioe) {
            } catch (InterruptedException ie) {
            }
            tcpSocket = null;
            udpSocket = null;
            serverSocket = null;
            multicastSocket = null;
        }

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

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

            try {
                serverSocket = new ServerSocket();
                serverSocket.setReuseAddress(true);
                serverSocket.bind(new InetSocketAddress(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();
                }
            }
        }

        // Multicast handlers
        public int joinGroup(String address) {
            try {
                if (type != SocketType.UDP) {
                    Log.e(LOG_TAG, "joinGroup not allowed with TCP sockets.");
                    return -5; // INVALID_HANDLE
                }

                if (multicastSocket == null) {
                    if (!bound) {
                        Log.e(LOG_TAG, "joinGroup not allowed unless socket is bound to a port");
                        return -15; // SOCKET_NOT_CONNECTED
                    }

                    // Unbind and destroy my UDP socket, since it's no longer needed.
                    udpSocket.close();
                    udpSocket = null;

                    multicastSocket = new MulticastSocket(localPort);
                    multicastGroups = new HashSet<String>();
                    multicast = true;
                }

                if (multicastGroups.contains(address)) {
                    Log.e(LOG_TAG, "Attempted to join an already joined multicast group.");
                    return -147; // ADDRESS_IN_USE
                }

                multicastGroups.add(address);
                multicastSocket.joinGroup(InetAddress.getByName(address));
                return 0;
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "Exception while joining multicast group: " + ioe);
                return -2; // FAILED
            }
        }

        public int leaveGroup(String address) {
            try {
                if (!multicast) {
                    Log.e(LOG_TAG, "Not a multicast socket");
                    return -2; // FAILED
                }

                if (multicastGroups.contains(address)) {
                    multicastGroups.remove(address);
                    multicastSocket.leaveGroup(InetAddress.getByName(address));
                    return 0;
                }
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "Exception while leaving multicast group: " + ioe);
            }

            return -2; // FAILED
        }

        public int setMulticastTimeToLive(int ttl) {
            try {
                if (multicast) {
                    if (0 <= ttl && ttl <= 255) {
                        multicastSocket.setTimeToLive(ttl);
                        return 0;
                    } else {
                        return -4; // INVALID_ARGUMENT
                    }
                }
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "Exception while setting multicast TTL: " + ioe);
            }
            return -2; // FAILED
        }

        public int setMulticastLoopbackMode(boolean enabled) {
            try {
                if (multicast) {
                    multicastSocket.setLoopbackMode(!enabled);
                    return 0;
                }
            } catch (IOException ioe) {
                Log.e(LOG_TAG, "Exception while setting multicast loopback mode: " + ioe);
            }
            return -2; // FAILED
        }

        public Collection<String> getJoinedGroups() {
            return multicastGroups;
        }

        private class ConnectThread extends Thread {
            private CallbackContext callbackContext;
            private InetAddress address;
            private int port;

            public ConnectThread(InetAddress address, int port, CallbackContext callbackContext) {
                this.address = address;
                this.port = port;
                this.callbackContext = callbackContext;
            }

            public void run() {
                try {
                    SocketData.this.tcpSocket = new Socket(this.address, this.port);
                    SocketData.this.init();

                    callbackContext.success();
                } catch (Exception e) {
                    callbackContext.error("An error occurred");
                }
            }
        }

        private 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;
                        byte[] outResized;
                        int bytesRead;

                        if (type == SocketType.TCP) {
                            try {
                                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;
                                }

                                if (bytesRead < toRead) {
                                    outResized = new byte[bytesRead];
                                    System.arraycopy(out, 0, outResized, 0, bytesRead);
                                    readData.context.success(outResized);
                                } else {
                                    readData.context.success(out);
                                }
                            } catch (NullPointerException e) {
                                SocketData.this.readQueue.put(readData);
                            } catch (SocketException e) {
                                readData.context.error("Socket closed");
                            }
                        } else {
                            if (toRead > 0) {
                                out = new byte[toRead];
                            } else {
                                out = new byte[4096]; // Defaults to 4K chunks.
                            }

                            DatagramPacket packet = new DatagramPacket(out, out.length);
                            if (multicastSocket != null && readData.recvFrom) {
                                multicastSocket.receive(packet);
                            } else {
                                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 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.this.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
}