org.xbmc.kore.jsonrpc.HostConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.xbmc.kore.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 org.xbmc.kore.jsonrpc;

import android.os.Handler;
import android.os.Process;
import android.text.TextUtils;

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.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.notification.Input;
import org.xbmc.kore.jsonrpc.notification.Player;
import org.xbmc.kore.jsonrpc.notification.System;
import org.xbmc.kore.utils.LogUtils;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 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

    /**
     * OkHttpClient. Make sure it is initialized, by calling {@link #getOkHttpClient()}
     */
    private OkHttpClient httpClient = null;
    private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");

    /**
     * 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);
                    executeThroughOkHttp(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.onSuccess(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);
    //      }
    //   }

    /**
     * Sends the JSON RPC request through HTTP (using OkHttp library)
     */
    private <T> void executeThroughOkHttp(final ApiMethod<T> method, final ApiCallback<T> callback,
            final Handler handler) {
        OkHttpClient client = getOkHttpClient();
        String jsonRequest = method.toJsonString();

        try {
            Request request = new Request.Builder().url(hostInfo.getJsonRpcHttpEndpoint())
                    .post(RequestBody.create(MEDIA_TYPE_JSON, jsonRequest)).build();
            LogUtils.LOGD(TAG, "Sending request via OkHttp: " + jsonRequest);
            Response response = sendOkHttpRequest(client, request);
            final T result = method.resultFromJson(parseJsonResponse(handleOkHttpResponse(response)));

            if ((handler != null) && (callback != null)) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSuccess(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());
                    }
                });
            }
        }
    }

    /**
     * Initializes this class OkHttpClient
     */
    public OkHttpClient getOkHttpClient() {
        if (httpClient == null) {
            httpClient = new OkHttpClient();
            httpClient.setConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS);

            httpClient.setAuthenticator(new Authenticator() {
                @Override
                public Request authenticate(Proxy proxy, Response response) throws IOException {
                    if (TextUtils.isEmpty(hostInfo.getUsername()))
                        return null;

                    String credential = Credentials.basic(hostInfo.getUsername(), hostInfo.getPassword());
                    return response.request().newBuilder().header("Authorization", credential).build();
                }

                @Override
                public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
                    return null;
                }
            });
        }
        return httpClient;
    }

    // Hack to circumvent a Protocol Exception that occurs when the server returns bogus Status Line
    // http://forum.kodi.tv/showthread.php?tid=224288
    private OkHttpClient getNewOkHttpClientNoKeepAlive() {
        java.lang.System.setProperty("http.keepAlive", "false");
        httpClient = null;
        return getOkHttpClient();
    }

    /**
     * Send an OkHttp POST request
     * @param request Request to send
     * @throws ApiException
     */
    private Response sendOkHttpRequest(final OkHttpClient client, final Request request) throws ApiException {
        try {
            return client.newCall(request).execute();
        } catch (ProtocolException e) {
            LogUtils.LOGW(TAG, "Got a Protocol Exception when trying to send OkHttp request. "
                    + "Trying again without connection pooling to try to circunvent this", e);
            // Hack to circumvent a Protocol Exception that occurs when the server returns bogus Status Line
            // http://forum.kodi.tv/showthread.php?tid=224288
            httpClient = getNewOkHttpClientNoKeepAlive();
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e);
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to send OkHttp request.", e);
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e);
        } catch (RuntimeException e) {
            // Seems like OkHttp throws a RuntimeException when it gets a malformed URL
            LogUtils.LOGW(TAG, "Got a Runtime exception when sending OkHttp request. Probably a malformed URL.", e);
            throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e);
        }
    }

    /**
     * Reads the response from the server
     * @param response Response from OkHttp
     * @return Response body string
     * @throws ApiException
     */
    private String handleOkHttpResponse(Response response) throws ApiException {
        try {
            //         LogUtils.LOGD(TAG, "Reading HTTP response.");
            int responseCode = response.code();

            switch (responseCode) {
            case 200:
                // All ok, read response
                String res = response.body().string();
                response.body().close();
                LogUtils.LOGD(TAG, "OkHTTP response: " + res);
                return res;
            case 401:
                LogUtils.LOGD(TAG, "OkHTTP response read error. Got a 401: " + response);
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED,
                        "Server returned response code: " + response);
            case 404:
                LogUtils.LOGD(TAG, "OkHTTP response read error. Got a 404: " + response);
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND,
                        "Server returned response code: " + response);
            default:
                LogUtils.LOGD(TAG, "OkHTTP response read error. Got: " + response);
                throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN,
                        "Server returned response code: " + response);
            }
        } catch (IOException e) {
            LogUtils.LOGW(TAG, "Failed to read OkHTTP 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 {
                        @SuppressWarnings("unchecked")
                        final T result = (T) methodCallInfo.method.resultFromJson(jsonResponse);
                        @SuppressWarnings("unchecked")
                        final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;

                        if ((methodCallInfo.handler != null) && (callback != null)) {
                            methodCallInfo.handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onSuccess(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) {
                    @SuppressWarnings("unchecked")
                    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);
                    @SuppressWarnings("unchecked")
                    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 protocol 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;
        }
    }
}