com.syncedsynapse.kore2.jsonrpc.HostConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.syncedsynapse.kore2.jsonrpc.HostConnection.java

Source

/*
 * Copyright 2015 Synced Synapse. All rights reserved.
 *
 * Licensed 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 com.syncedsynapse.kore2.jsonrpc;

import android.os.Handler;
import android.os.Process;
import android.util.Base64;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.syncedsynapse.kore2.host.HostInfo;
import com.syncedsynapse.kore2.jsonrpc.notification.Input;
import com.syncedsynapse.kore2.jsonrpc.notification.Player;
import com.syncedsynapse.kore2.jsonrpc.notification.System;
import com.syncedsynapse.kore2.utils.LogUtils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.URL;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Class responsible for communicating with the host.
 */
public class HostConnection {
    public static final String TAG = LogUtils.makeLogTag(HostConnection.class);

    /**
     * Communicate via TCP
     */
    public static final int PROTOCOL_TCP = 0;
    /**
     * Communicate via HTTP
     */
    public static final int PROTOCOL_HTTP = 1;

    /**
     * Interface that an observer must implement to be notified of player notifications
     */
    public interface PlayerNotificationsObserver {
        public void onPlay(Player.OnPlay notification);

        public void onPause(Player.OnPause notification);

        public void onSpeedChanged(Player.OnSpeedChanged notification);

        public void onSeek(Player.OnSeek notification);

        public void onStop(Player.OnStop notification);
    }

    /**
     * Interface that an observer must implement to be notified of System notifications
     */
    public interface SystemNotificationsObserver {
        public void onQuit(System.OnQuit notification);

        public void onRestart(System.OnRestart notification);

        public void onSleep(System.OnSleep notification);
    }

    /**
     * Interface that an observer must implement to be notified of Input notifications
     */
    public interface InputNotificationsObserver {
        public void onInputRequested(Input.OnInputRequested notification);
    }

    /**
    * Host to connect too
    */
    private final HostInfo hostInfo;

    /**
     * The protocol to use: {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP}
     * This is initially obtained from the {@link HostInfo}, but can be later changed through
     * {@link #setProtocol(int)}
     */
    private int protocol;

    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * Socket used to communicate through TCP
     */
    private Socket socket = null;
    /**
     * Listener {@link Thread} that will be listening on the TCP socket
     */
    private Thread listenerThread = null;

    /**
     * {@link java.util.HashMap} that will hold the {@link MethodCallInfo} with the information
     * necessary to respond to clients (TCP only)
     */
    private final HashMap<String, MethodCallInfo<?>> clientCallbacks = new HashMap<String, MethodCallInfo<?>>();

    /**
     * The observers that will be notified of player notifications
     */
    private final HashMap<PlayerNotificationsObserver, Handler> playerNotificationsObservers = new HashMap<PlayerNotificationsObserver, Handler>();

    /**
     * The observers that will be notified of system notifications
     */
    private final HashMap<SystemNotificationsObserver, Handler> systemNotificationsObservers = new HashMap<SystemNotificationsObserver, Handler>();

    /**
     * The observers that will be notified of input notifications
     */
    private final HashMap<InputNotificationsObserver, Handler> inputNotificationsObservers = new HashMap<InputNotificationsObserver, Handler>();

    private ExecutorService executorService;

    private final int connectTimeout;

    private static final int DEFAULT_CONNECT_TIMEOUT = 5000; // ms

    private static final int TCP_READ_TIMEOUT = 30000; // ms

    /**
     * Creates a new host connection
     * @param hostInfo Host info object
     */
    public HostConnection(final HostInfo hostInfo) {
        this(hostInfo, DEFAULT_CONNECT_TIMEOUT);
    }

    /**
     * Creates a new host connection
     * @param hostInfo Host info object
     * @param connectTimeout Connection timeout in ms
     */
    public HostConnection(final HostInfo hostInfo, int connectTimeout) {
        this.hostInfo = hostInfo;
        // Start with the default host protocol
        this.protocol = hostInfo.getProtocol();
        // Create a single threaded executor
        this.executorService = Executors.newSingleThreadExecutor();
        // Set timeout
        this.connectTimeout = connectTimeout;
    }

    /**
     * Returns this connection protocol
     * @return {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP}
     */
    public int getProtocol() {
        return protocol;
    }

    /**
     * Overrides the protocol for this connection
     * @param protocol {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP}
     */
    public void setProtocol(int protocol) {
        if (!isValidProtocol(protocol)) {
            throw new IllegalArgumentException("Invalid protocol specified.");
        }
        this.protocol = protocol;
    }

    public static boolean isValidProtocol(int protocol) {
        return ((protocol == PROTOCOL_TCP) || (protocol == PROTOCOL_HTTP));
    }

    /**
     * Registers an observer for player notifications
     * @param observer The {@link PlayerNotificationsObserver}
     */
    public void registerPlayerNotificationsObserver(PlayerNotificationsObserver observer, Handler handler) {
        playerNotificationsObservers.put(observer, handler);
    }

    /**
     * Unregisters and observer from the player notifications
     * @param observer The {@link PlayerNotificationsObserver} to unregister
     */
    public void unregisterPlayerNotificationsObserver(PlayerNotificationsObserver observer) {
        playerNotificationsObservers.remove(observer);
    }

    /**
     * Registers an observer for system notifications
     * @param observer The {@link SystemNotificationsObserver}
     */
    public void registerSystemNotificationsObserver(SystemNotificationsObserver observer, Handler handler) {
        systemNotificationsObservers.put(observer, handler);
    }

    /**
     * Unregisters and observer from the system notifications
     * @param observer The {@link SystemNotificationsObserver}
     */
    public void unregisterSystemNotificationsObserver(SystemNotificationsObserver observer) {
        systemNotificationsObservers.remove(observer);
    }

    /**
     * Registers an observer for input notifications
     * @param observer The {@link InputNotificationsObserver}
     */
    public void registerInputNotificationsObserver(InputNotificationsObserver observer, Handler handler) {
        inputNotificationsObservers.put(observer, handler);
    }

    /**
     * Unregisters and observer from the input notifications
     * @param observer The {@link InputNotificationsObserver}
     */
    public void unregisterInputNotificationsObserver(InputNotificationsObserver observer) {
        inputNotificationsObservers.remove(observer);
    }

    /**
    * Calls the a method on the server
    * This call is always asynchronous. The results will be posted, through the
    * {@link ApiCallback callback} parameter, on the specified {@link android.os.Handler}.
    *
    * @param method Method object that represents the methood too call
    * @param callback {@link ApiCallback} to post the response to
    * @param handler {@link Handler} to invoke callbacks on
    * @param <T> Method return type
    */
    public <T> void execute(final ApiMethod<T> method, final ApiCallback<T> callback, final Handler handler) {
        LogUtils.LOGD(TAG, "Starting method execute. Method: " + method.getMethodName() + " on host: "
                + hostInfo.getJsonRpcHttpEndpoint());

        // Launch background thread
        Runnable command = new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                if (protocol == PROTOCOL_HTTP) {
                    executeThroughHTTP(method, callback, handler);
                } else {
                    executeThroughTcp(method, callback, handler);
                }
            }
        };

        executorService.execute(command);
        //new Thread(command).start();
    }

    /**
     * Sends the JSON RPC request through HTTP
     */
    private <T> void executeThroughHTTP(final ApiMethod<T> method, final ApiCallback<T> callback,
            final Handler handler) {
        String jsonRequest = method.toJsonString();
        try {
            HttpURLConnection connection = openHttpConnection(hostInfo);
            sendHttpRequest(connection, jsonRequest);
            // Read response and convert it
            final T result = method.resultFromJson(parseJsonResponse(readHttpResponse(connection)));

            if ((handler != null) && (callback != null)) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSucess(result);
                    }
                });
            }
        } catch (final ApiException e) {
            // Got an error, call error handler

            if ((handler != null) && (callback != null)) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onError(e.getCode(), e.getMessage());
                    }
                });
            }
        }
    }

    /**
     * Sends the JSON RPC request through HTTP, and calls the callback with the raw response,
     * not parsed into the internal representation.
     * Useful for sync methods that don't want to incur the overhead of constructing the
     * internal objects.
     *
     * @param method Method object that represents the method too call
     * @param callback {@link ApiCallback} to post the response to. This will be the raw
     * {@link ObjectNode} received
     * @param handler {@link Handler} to invoke callbacks on
     * @param <T> Method return type
     */
    public <T> void executeRaw(final ApiMethod<T> method, final ApiCallback<ObjectNode> callback,
            final Handler handler) {
        String jsonRequest = method.toJsonString();
        try {
            HttpURLConnection connection = openHttpConnection(hostInfo);
            sendHttpRequest(connection, jsonRequest);
            // Read response and convert it
            final ObjectNode result = parseJsonResponse(readHttpResponse(connection));

            if ((handler != null) && (callback != null)) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSucess(result);
                    }
                });
            }
        } catch (final ApiException e) {
            // Got an error, call error handler
            if ((handler != null) && (callback != null)) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onError(e.getCode(), e.getMessage());
                    }
                });
            }
        }
    }

    /**
    * Auxiliary method to open a HTTP connection.
    * This method calls connect() so that any errors are cathced
    * @param hostInfo Host info
    * @return Connection set up
    * @throws ApiException
    */
    private HttpURLConnection openHttpConnection(HostInfo hostInfo) throws ApiException {
        try {
            //         LogUtils.LOGD(TAG, "Opening HTTP connection.");
            HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint())
                    .openConnection();
            connection.setRequestMethod("POST");
            connection.setConnectTimeout(connectTimeout);
            //connection.setReadTimeout(connectTimeout);
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setDoOutput(true);

            // http basic authorization
            if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty()
                    && (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) {
                final String token = Base64.encodeToString(
                        (hostInfo.getUsername() + ":" + hostInfo.getPassword()).getBytes(), Base64.DEFAULT);
                connection.setRequestProperty("Authorization", "Basic " + token);
            }

            // Check the connection
            connection.connect();
            return connection;
        } catch (ProtocolException e) {
            // Won't try to catch this
            LogUtils.LOGE(TAG, "Got protocol exception while opening HTTP connection.", e);
            throw new RuntimeException(e);
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to open HTTP connection.", e);
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e);
        }
    }

    /**
     * Send an HTTP POST request
     * @param connection Open connection
     * @param request Request to send
     * @throws ApiException
     */
    private void sendHttpRequest(HttpURLConnection connection, String request) throws ApiException {
        try {
            LogUtils.LOGD(TAG, "Sending request via HTTP: " + request);
            OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
            out.write(request);
            out.flush();
            out.close();
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to send HTTP request.", e);
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e);
        }

    }

    /**
     * Reads the response from the server
     * @param connection Connection
     * @return Response read
     * @throws ApiException
     */
    private String readHttpResponse(HttpURLConnection connection) throws ApiException {
        try {
            //         LogUtils.LOGD(TAG, "Reading HTTP response.");
            int responseCode = connection.getResponseCode();

            switch (responseCode) {
            case 200:
                // All ok, read response
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                StringBuilder response = new StringBuilder();
                String inputLine;
                while ((inputLine = in.readLine()) != null)
                    response.append(inputLine);
                in.close();
                LogUtils.LOGD(TAG, "HTTP response: " + response.toString());
                return response.toString();
            case 401:
                LogUtils.LOGD(TAG, "HTTP response read error. Got a 401.");
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED,
                        "Server returned response code: " + responseCode);
            case 404:
                LogUtils.LOGD(TAG, "HTTP response read error. Got a 404.");
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND,
                        "Server returned response code: " + responseCode);
            default:
                LogUtils.LOGD(TAG, "HTTP response read error. Got: " + responseCode);
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN,
                        "Server returned response code: " + responseCode);
            }
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to read HTTP response.", e);
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e);
        }
    }

    /**
     * Parses the JSON response from the server.
     * If it is a valid result returns the JSON {@link com.fasterxml.jackson.databind.node.ObjectNode} that represents it.
     * If it is an error (contains the error tag), returns an {@link ApiException} with the info.
     * @param response JSON response
     * @return {@link com.fasterxml.jackson.databind.node.ObjectNode} constructed
     * @throws ApiException
     */
    private ObjectNode parseJsonResponse(String response) throws ApiException {
        //      LogUtils.LOGD(TAG, "Parsing JSON response");
        try {
            ObjectNode jsonResponse = (ObjectNode) objectMapper.readTree(response);

            if (jsonResponse.has(ApiMethod.ERROR_NODE)) {
                throw new ApiException(ApiException.API_ERROR, jsonResponse);
            }

            if (!jsonResponse.has(ApiMethod.RESULT_NODE)) {
                // Something strange is going on
                throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST,
                        "Result doesn't contain a result node.");
            }

            return jsonResponse;
        } catch (JsonProcessingException e) {
            LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e);
            throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e);
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e);
            throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e);
        }
    }

    /**
     * Sends the JSON RPC request through TCP
     * Keeps a background thread running, listening on a socket
     */
    private <T> void executeThroughTcp(final ApiMethod<T> method, final ApiCallback<T> callback,
            final Handler handler) {
        String methodId = String.valueOf(method.getId());
        try {
            // Save this method/callback for later response
            // Check if a method with this id is already running and raise an error if so
            synchronized (clientCallbacks) {
                if (clientCallbacks.containsKey(methodId)) {
                    if ((handler != null) && (callback != null)) {
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
                                        "A method with the same Id is already executing");
                            }
                        });
                    }
                    return;
                }
                clientCallbacks.put(methodId, new MethodCallInfo<T>(method, callback, handler));
            }

            // TODO: Validate if this shouldn't be enclosed by a synchronized.
            if (socket == null) {
                // Open connection to the server and setup reader thread
                socket = openTcpConnection(hostInfo);
                listenerThread = newListenerThread(socket);
                listenerThread.start();
            }

            // Write request
            sendTcpRequest(socket, method.toJsonString());
        } catch (final ApiException e) {
            callErrorCallback(methodId, e);
        }
    }

    /**
     * Auxiliary method to open the TCP {@link Socket}.
     * This method calls connect() so that any errors are cathced
     * @param hostInfo Host info
     * @return Connection set up
     * @throws ApiException
     */
    private Socket openTcpConnection(HostInfo hostInfo) throws ApiException {
        try {
            LogUtils.LOGD(TAG, "Opening TCP connection on host: " + hostInfo.getAddress());

            Socket socket = new Socket();
            final InetSocketAddress address = new InetSocketAddress(hostInfo.getAddress(), hostInfo.getTcpPort());
            // We're setting a read timeout on the socket, so no need to explicitly close it
            socket.setSoTimeout(TCP_READ_TIMEOUT);
            socket.connect(address, connectTimeout);

            return socket;
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress());
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e);
        }
    }

    /**
     * Send a TCP request
     * @param socket Socket to write to
     * @param request Request to send
     * @throws ApiException
     */
    private void sendTcpRequest(Socket socket, String request) throws ApiException {
        try {
            LogUtils.LOGD(TAG, "Sending request via TCP: " + request);
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            writer.write(request);
            writer.flush();
        } catch (Exception e) {
            LogUtils.LOGW(TAG, "Failed to send TCP request.", e);
            disconnect();
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e);
        }
    }

    private Thread newListenerThread(final Socket socket) {
        // Launch a new thread to read from the socket
        return new Thread(new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                try {
                    LogUtils.LOGD(TAG, "Starting socket listener thread...");
                    // We're going to read from the socket. This will be a blocking call and
                    // it will keep on going until disconnect() is called on this object.
                    // Note: Mind the objects used here: we use createParser because it doesn't
                    // close the socket after ObjectMapper.readTree.
                    JsonParser jsonParser = objectMapper.getFactory().createParser(socket.getInputStream());
                    ObjectNode jsonResponse;
                    while ((jsonResponse = objectMapper.readTree(jsonParser)) != null) {
                        LogUtils.LOGD(TAG, "Read from socket: " + jsonResponse.toString());
                        //                        LogUtils.LOGD_FULL(TAG, "Read from socket: " + jsonResponse.toString());
                        handleTcpResponse(jsonResponse);
                    }
                } catch (JsonProcessingException e) {
                    LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e);
                    callErrorCallback(null, new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e));
                } catch (IOException e) {
                    LogUtils.LOGW(TAG, "Error reading from socket.", e);
                    disconnect();
                    callErrorCallback(null, new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e));
                }
            }
        });
    }

    private <T> void handleTcpResponse(ObjectNode jsonResponse) {

        if (!jsonResponse.has(ApiMethod.ID_NODE)) {
            // It's a notification, notify observers
            String notificationName = jsonResponse.get(ApiNotification.METHOD_NODE).asText();
            ObjectNode params = (ObjectNode) jsonResponse.get(ApiNotification.PARAMS_NODE);

            if (notificationName.equals(Player.OnPause.NOTIFICATION_NAME)) {
                final Player.OnPause apiNotification = new Player.OnPause(params);
                for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) {
                    Handler handler = playerNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onPause(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(Player.OnPlay.NOTIFICATION_NAME)) {
                final Player.OnPlay apiNotification = new Player.OnPlay(params);
                for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) {
                    Handler handler = playerNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onPlay(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(Player.OnSeek.NOTIFICATION_NAME)) {
                final Player.OnSeek apiNotification = new Player.OnSeek(params);
                for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) {
                    Handler handler = playerNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onSeek(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(Player.OnSpeedChanged.NOTIFICATION_NAME)) {
                final Player.OnSpeedChanged apiNotification = new Player.OnSpeedChanged(params);
                for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) {
                    Handler handler = playerNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onSpeedChanged(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(Player.OnStop.NOTIFICATION_NAME)) {
                final Player.OnStop apiNotification = new Player.OnStop(params);
                for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) {
                    Handler handler = playerNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onStop(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(System.OnQuit.NOTIFICATION_NAME)) {
                final System.OnQuit apiNotification = new System.OnQuit(params);
                for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) {
                    Handler handler = systemNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onQuit(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(System.OnRestart.NOTIFICATION_NAME)) {
                final System.OnRestart apiNotification = new System.OnRestart(params);
                for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) {
                    Handler handler = systemNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onRestart(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(System.OnSleep.NOTIFICATION_NAME)) {
                final System.OnSleep apiNotification = new System.OnSleep(params);
                for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) {
                    Handler handler = systemNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onSleep(apiNotification);
                        }
                    });
                }
            } else if (notificationName.equals(Input.OnInputRequested.NOTIFICATION_NAME)) {
                final Input.OnInputRequested apiNotification = new Input.OnInputRequested(params);
                for (final InputNotificationsObserver observer : inputNotificationsObservers.keySet()) {
                    Handler handler = inputNotificationsObservers.get(observer);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            observer.onInputRequested(apiNotification);
                        }
                    });
                }
            }

            LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue());
        } else {
            String methodId = jsonResponse.get(ApiMethod.ID_NODE).asText();

            if (jsonResponse.has(ApiMethod.ERROR_NODE)) {
                // Error response
                callErrorCallback(methodId, new ApiException(ApiException.API_ERROR, jsonResponse));
            } else {
                // Sucess response
                final MethodCallInfo<?> methodCallInfo = clientCallbacks.get(methodId);
                //            LogUtils.LOGD(TAG, "Sending response to method: " + methodCallInfo.method.getMethodName());

                if (methodCallInfo != null) {
                    try {
                        final T result = (T) methodCallInfo.method.resultFromJson(jsonResponse);
                        final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;

                        if ((methodCallInfo.handler != null) && (callback != null)) {
                            methodCallInfo.handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onSucess(result);
                                }
                            });
                        }

                        // We've replied, remove the client from the list
                        synchronized (clientCallbacks) {
                            clientCallbacks.remove(methodId);
                        }
                    } catch (ApiException e) {
                        callErrorCallback(methodId, e);
                    }
                }
            }
        }
    }

    private <T> void callErrorCallback(String methodId, final ApiException error) {
        synchronized (clientCallbacks) {
            if (methodId != null) {
                // Send error back to client
                final MethodCallInfo<?> methodCallInfo = clientCallbacks.get(methodId);
                if (methodCallInfo != null) {
                    final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;

                    if ((methodCallInfo.handler != null) && (callback != null)) {
                        methodCallInfo.handler.post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onError(error.getCode(), error.getMessage());
                            }
                        });
                    }
                }
                clientCallbacks.remove(methodId);
            } else {
                // Notify all pending clients, it might be an error for them
                for (String id : clientCallbacks.keySet()) {
                    final MethodCallInfo<?> methodCallInfo = clientCallbacks.get(id);
                    final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;

                    if ((methodCallInfo.handler != null) && (callback != null)) {
                        methodCallInfo.handler.post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onError(error.getCode(), error.getMessage());
                            }
                        });
                    }
                }
                clientCallbacks.clear();
            }
        }
    }

    /**
     * Cleans up used resources.
     * This method should always be called if the protocoll used is TCP, so we can shutdown gracefully
     */
    public void disconnect() {
        if (protocol == PROTOCOL_HTTP)
            return;

        try {
            if (socket != null) {
                // Remove pending calls
                if (!socket.isClosed()) {
                    socket.close();
                }
            }
        } catch (IOException e) {
            LogUtils.LOGE(TAG, "Error while closing socket", e);
        } finally {
            socket = null;
        }
    }

    /**
     * Helper class to aggregate a method, callback and handler
     * @param <T>
     */
    private static class MethodCallInfo<T> {
        public final ApiMethod<T> method;
        public final ApiCallback<T> callback;
        public final Handler handler;

        public MethodCallInfo(ApiMethod<T> method, ApiCallback<T> callback, Handler handler) {
            this.method = method;
            this.callback = callback;
            this.handler = handler;
        }
    }
}