00001 00002 // * Copyright (C) 2005-2015 Team XBMC 00003 // * http://xbmc.org 00004 // * 00005 // * This Program is free software; you can redistribute it and/or modify 00006 // * it under the terms of the GNU General Public License as published by 00007 // * the Free Software Foundation; either version 2, or (at your option) 00008 // * any later version. 00009 // * 00010 // * This Program is distributed in the hope that it will be useful, 00011 // * but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // * GNU General Public License for more details. 00014 // * 00015 // * You should have received a copy of the GNU General Public License 00016 // * along with XBMC Remote; see the file license. If not, write to 00017 // * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. 00018 // * http://www.gnu.org/copyleft/gpl.html 00019 // * 00020 // */ 00021 // 00022 //package org.xbmc.android.jsonrpc.service; 00023 // 00024 //import java.io.BufferedWriter; 00025 //import java.io.EOFException; 00026 //import java.io.IOException; 00027 //import java.io.OutputStreamWriter; 00028 //import java.net.InetSocketAddress; 00029 //import java.net.Socket; 00030 //import java.net.SocketTimeoutException; 00031 //import java.net.UnknownHostException; 00032 //import java.util.ArrayList; 00033 //import java.util.HashMap; 00034 //import java.util.LinkedList; 00035 //import java.util.Timer; 00036 //import java.util.TimerTask; 00037 // 00038 //import org.codehaus.jackson.JsonFactory; 00039 //import org.codehaus.jackson.JsonNode; 00040 //import org.codehaus.jackson.JsonParseException; 00041 //import org.codehaus.jackson.JsonParser; 00042 //import org.codehaus.jackson.map.ObjectMapper; 00043 //import org.codehaus.jackson.node.ObjectNode; 00044 //import org.xbmc.android.jsonrpc.api.AbstractCall; 00045 //import org.xbmc.android.jsonrpc.io.ApiException; 00046 //import org.xbmc.android.jsonrpc.io.ConnectionManager; 00047 //import org.xbmc.android.jsonrpc.io.JsonHandler; 00048 //import org.xbmc.android.jsonrpc.notification.AbstractEvent; 00049 // 00050 //import android.app.IntentService; 00051 //import android.content.Intent; 00052 //import android.os.Bundle; 00053 //import android.os.Handler; 00054 //import android.os.IBinder; 00055 //import android.os.Message; 00056 //import android.os.Messenger; 00057 //import android.os.RemoteException; 00058 //import android.util.Log; 00059 // 00061 // * Service which keeps a steady TCP connection to XBMC's JSON-RPC API via TCP 00062 // * socket (as opposed to HTTP messages). 00063 // * <p/> 00064 // * It serves as listener for notification, but is also used for posting normal 00065 // * API requests. 00066 // * <p/> 00067 // * Generally speaking, the service will shut down and terminate the TCP 00068 // * connection as soon as there are no more connected clients. However, clients 00069 // * may want to query several consecutive requests without having the service 00070 // * stop and restart between every request. That is the reason why there is a 00071 // * "cooldown" period, in which the service will just wait for new clients to 00072 // * arrive before shutting down. 00073 // * <p/> 00074 // * About message exchange, see {@link ConnectionManager}. 00075 // * 00076 // * @author freezy <freezy@xbmc.org> 00077 // */ 00078 //public class ConnectionService extends IntentService { 00079 // 00080 // public final static String TAG = ConnectionService.class.getSimpleName(); 00081 // 00082 // private static final int SOCKET_TIMEOUT = 5000; 00083 // 00084 // public static final String EXTRA_ADDRESS = "org.xbmc.android.jsonprc.extra.ADDRESS"; 00085 // public static final String EXTRA_TCPPORT = "org.xbmc.android.jsonprc.extra.TCPPORT"; 00086 // public static final String EXTRA_HTTPPORT = "org.xbmc.android.jsonprc.extra.HTTPPORT"; 00087 // 00088 // public static final String EXTRA_STATUS = "org.xbmc.android.jsonprc.extra.STATUS"; 00089 // public static final String EXTRA_APICALL = "org.xbmc.android.jsonprc.extra.APICALL"; 00090 // public static final String EXTRA_NOTIFICATION = "org.xbmc.android.jsonprc.extra.NOTIFICATION"; 00091 // public static final String EXTRA_HANDLER = "org.xbmc.android.jsonprc.extra.HANDLER"; 00092 // public static final String EXTRA_CALLID = "org.xbmc.android.jsonprc.extra.CALLID"; 00093 // 00094 // public static final int MSG_REGISTER_CLIENT = 0x01; 00095 // public static final int MSG_UNREGISTER_CLIENT = 0x02; 00096 // public static final int MSG_CONNECTING = 0x03; 00097 // public static final int MSG_CONNECTED = 0x04; 00098 // public static final int MSG_RECEIVE_NOTIFICATION = 0x05; 00099 // public static final int MSG_RECEIVE_APICALL = 0x06; 00100 // public static final int MSG_RECEIVE_HANDLED_APICALL = 0x07; 00101 // public static final int MSG_SEND_APICALL = 0x08; 00102 // public static final int MSG_SEND_HANDLED_APICALL = 0x09; 00103 // public static final int MSG_ERROR = 0x0a; 00104 // 00105 // public static final int RESULT_SUCCESS = 0x01; 00106 // 00107 // /** 00108 // * Time in milliseconds we wait for new requests until we shut down the 00109 // * service (and connection). 00110 // */ 00111 // private static final long COOLDOWN = 10000; 00112 // 00113 // /** 00114 // * Target we publish for clients to send messages to IncomingHandler. 00115 // */ 00116 // private final Messenger mMessenger = new Messenger(new IncomingHandler()); 00117 // 00118 // /** 00119 // * Keeps track of all currently registered client. Normally, all clients 00120 // * are {@link ConnectionManager} instances. 00121 // */ 00122 // private final ArrayList<Messenger> mClients = new ArrayList<Messenger>(); 00123 // /** 00124 // * API call results are only returned to the client requested it, so here are the relations. 00125 // */ 00126 // private final HashMap<String, Messenger> mClientMap = new HashMap<String, Messenger>(); 00127 // /** 00128 // * If we have to send data before we're connected, store data until connection 00129 // */ 00130 // private final LinkedList<AbstractCall<?>> mPendingCalls = new LinkedList<AbstractCall<?>>(); 00131 // /** 00132 // * All calls the service is currently dealing with. Key is the ID of the call. 00133 // */ 00134 // private final HashMap<String, AbstractCall<?>> mCalls = new HashMap<String, AbstractCall<?>>(); 00135 // /** 00136 // * The handler we'll update with a status code as soon as we're done. 00137 // */ 00138 // private final HashMap<String, JsonHandler> mHandlers = new HashMap<String, JsonHandler>(); 00139 // 00140 // /** 00141 // * Reference to the socket, so we shut it down properly. 00142 // */ 00143 // private Socket mSocket = null; 00144 // /** 00145 // * Output writer so we can also write stuff to the socket. 00146 // */ 00147 // private BufferedWriter mOut = null; 00148 // 00149 // /** 00150 // * Static reference to Jackson's object mapper. 00151 // */ 00152 // private final static ObjectMapper OM = new ObjectMapper(); 00153 // 00154 // /** 00155 // * When no more clients are connected, wait {@link #COOLDOWN} milliseconds 00156 // * and then shut down the service if no new clients connect. 00157 // */ 00158 // private Timer mCooldownTimer = null; 00159 // 00160 // 00161 // /** 00162 // * Host name or IP address of XBMC 00163 // */ 00164 // private String mAddress; 00165 // /** 00166 // * Port where the TCP service is running (default 9090). 00167 // */ 00168 // private int mPort; 00169 // 00170 // /** 00171 // * Class constructor must be empty for services. 00172 // */ 00173 // public ConnectionService() { 00174 // super(TAG); 00175 // } 00176 // 00177 // @Override 00178 // protected void onHandleIntent(Intent intent) { 00179 // 00180 // mPort = intent.getIntExtra(EXTRA_TCPPORT, 9090); 00181 // mAddress = intent.getStringExtra(EXTRA_ADDRESS) != null ? intent.getStringExtra(EXTRA_ADDRESS) : "10.0.2.2"; 00182 // 00183 // long s = System.currentTimeMillis(); 00184 // Log.d(TAG, "Starting TCP client..."); 00185 // notifyStatus(MSG_CONNECTING, null); 00186 // Socket socket = null; 00187 // 00188 // try { 00189 // final InetSocketAddress sockaddr = new InetSocketAddress(mAddress, mPort); 00190 // socket = new Socket(); 00191 // mSocket = socket; // update class reference 00192 // socket.setSoTimeout(0); // no timeout for reading from connection. 00193 // socket.connect(sockaddr, SOCKET_TIMEOUT); 00194 // mOut = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 00195 // } catch (UnknownHostException e) { 00196 // Log.e(TAG, "Unknown host: " + e.getMessage(), e); 00197 // notifyError(new ApiException(ApiException.IO_UNKNOWN_HOST, "Unknown host: " + e.getMessage(), e), null); 00198 // stopSelf(); 00199 // return; 00200 // } catch (SocketTimeoutException e) { 00201 // Log.e(TAG, "Unknown host: " + e.getMessage(), e); 00202 // notifyError(new ApiException(ApiException.IO_SOCKETTIMEOUT, "Connection timeout: " + e.getMessage(), e), null); 00203 // stopSelf(); 00204 // return; 00205 // } catch (IOException e) { 00206 // Log.e(TAG, "I/O error while opening: " + e.getMessage(), e); 00207 // notifyError(new ApiException(ApiException.IO_EXCEPTION_WHILE_OPENING, "I/O error while opening: " + e.getMessage(), e), null); 00208 // stopSelf(); 00209 // return; 00210 // } 00211 // 00212 // try { 00213 // Log.i(TAG, "Connected to TCP socket in " + (System.currentTimeMillis() - s) + "ms"); 00214 // notifyStatus(MSG_CONNECTED, null); 00215 // 00216 // // check for saved post data to send while we weren't connected, 00217 // // but do it in a separate thread so we can read already while sending. 00218 // if (!mPendingCalls.isEmpty()) { 00219 // new Thread("post-data-on-connection") { 00220 // @Override 00221 // public void run() { 00222 // final LinkedList<AbstractCall<?>> calls = mPendingCalls; 00223 // while (!calls.isEmpty()) { 00224 // writeSocket(calls.poll()); 00225 // } 00226 // }; 00227 // }.start(); 00228 // } 00229 // 00230 // final JsonFactory jf = OM.getJsonFactory(); 00231 // final JsonParser jp = jf.createJsonParser(socket.getInputStream()); 00232 // JsonNode node; 00233 // while ((node = OM.readTree(jp)) != null) { 00234 // final String debugDump = node.toString(); 00235 // if (debugDump.length() > 80) { 00236 // Log.i(TAG, "READ: " + debugDump.substring(0, 80) + "..."); 00237 // } else { 00238 // Log.i(TAG, "READ: " + debugDump); 00239 // } 00240 // notifyClients(node); 00241 // } 00242 // mOut.close(); 00243 // Log.i(TAG, "TCP socket closed."); 00244 // 00245 // } catch (JsonParseException e) { 00246 // Log.e(TAG, "Cannot parse JSON response: " + e.getMessage(), e); 00247 // notifyError(new ApiException(ApiException.JSON_EXCEPTION, "Error while parsing JSON response: " + e.getMessage(), e), null); 00248 // } catch (EOFException e) { 00249 // Log.i(TAG, "Connection broken, quitting."); 00250 // notifyError(new ApiException(ApiException.IO_DISCONNECTED, "Socket disconnected: " + e.getMessage(), e), null); 00251 // } catch (IOException e) { 00252 // Log.e(TAG, "I/O error while reading (" + e.getClass().getSimpleName() + "): " + e.getMessage(), e); 00253 // notifyError(new ApiException(ApiException.IO_EXCEPTION_WHILE_READING, "I/O error while reading: " + e.getMessage(), e), null); 00254 // } finally { 00255 // try { 00256 // if (socket != null) { 00257 // socket.close(); 00258 // } 00259 // if (mOut != null) { 00260 // mOut.close(); 00261 // } 00262 // } catch (IOException e) { 00263 // // do nothing. 00264 // } 00265 // } 00266 // } 00267 // 00268 // @Override 00269 // public IBinder onBind(Intent intent) { 00270 // Log.i(TAG, "Connection service bound to new client."); 00271 // return mMessenger.getBinder(); 00272 // } 00273 // 00274 // @Override 00275 // public void onDestroy() { 00276 // super.onDestroy(); 00277 // try { 00278 // final Socket socket = mSocket; 00279 // if (socket != null) { 00280 // if (socket.isConnected()) { 00281 // socket.shutdownInput(); 00282 // } 00283 // if (!socket.isClosed()) { 00284 // socket.close(); 00285 // } 00286 // } 00287 // } catch (IOException e) { 00288 // Log.e(TAG, "Error closing socket.", e); 00289 // } 00290 // Log.d(TAG, "Notification service destroyed."); 00291 // } 00292 // 00293 // /** 00294 // * Starts cooldown. If there is no new client for {@link #COOLDOWN} 00295 // * milliseconds, the service will shutdown, otherwise it will continue 00296 // * to run until there is another cooldown. 00297 // */ 00298 // private void cooldown() { 00299 // Log.i(TAG, "Starting service cooldown."); 00300 // mCooldownTimer = new Timer(); 00301 // mCooldownTimer.schedule(new TimerTask() { 00302 // @Override 00303 // public void run() { 00304 // if (mClients.isEmpty()) { 00305 // Log.i(TAG, "No new clients, shutting down service."); 00306 // stopSelf(); 00307 // } else { 00308 // Log.i(TAG, "Cooldown failed, got " + mClients.size() + " new client(s)."); 00309 // } 00310 // } 00311 // }, COOLDOWN); 00312 // } 00313 // 00314 // 00315 // /** 00316 // * Treats the received result. 00317 // * <p/> 00318 // * If an ID is found in the response, the API call object is retrieved, 00319 // * updated and sent back to the client. 00320 // * <p/> 00321 // * Otherwise, the notification object is sent to all clients. 00322 // * 00323 // * @param node JSON-serialized response 00324 // */ 00325 // private void notifyClients(JsonNode node) { 00326 // final ArrayList<Messenger> clients = mClients; 00327 // final HashMap<String, Messenger> map = mClientMap; 00328 // final HashMap<String, AbstractCall<?>> calls = mCalls; 00329 // final HashMap<String, JsonHandler> handlers = mHandlers; 00330 // 00331 // // check for errors 00332 // if (node.has("error")) { 00333 // notifyError(new ApiException(node), node.get("id").getValueAsText()); 00334 // 00335 // // check if notification or api call 00336 // } else if (node.has("id")) { 00337 // // it's api call. 00338 // final String id = node.get("id").getValueAsText(); 00339 // if (calls.containsKey(id)) { 00340 // final AbstractCall<?> call = calls.get(id); 00341 // if (handlers.containsKey(id)) { 00342 // // we got an provided handler, so apply it and send back status message. 00343 // try { 00344 // handlers.get(id).applyResult(node, getContentResolver()); 00345 // // get the right client to send back to 00346 // if (map.containsKey(id)) { 00347 // final Bundle b = new Bundle(); 00348 // b.putString(EXTRA_CALLID, call.getId()); 00349 // b.putInt(EXTRA_STATUS, RESULT_SUCCESS); 00350 // final Message msg = Message.obtain(null, MSG_RECEIVE_HANDLED_APICALL); 00351 // msg.setData(b); 00352 // try { 00353 // map.get(id).send(msg); 00354 // Log.i(TAG, "API call handled successfully, posting status back to client."); 00355 // } catch (RemoteException e) { 00356 // Log.e(TAG, "Error posting status back to client: " + e.getMessage(), e); 00357 // map.remove(id); 00358 // } finally { 00359 // // clean up 00360 // map.remove(id); 00361 // calls.remove(id); 00362 // } 00363 // } else { 00364 // Log.w(TAG, "Cannot find client in caller-mapping for " + id + ", dropping response (handled call)."); 00365 // } 00366 // } catch (ApiException e) { 00367 // notifyError(e, id); 00368 // } 00369 // } else { 00370 // // get the right client to send back to 00371 // if (map.containsKey(id)) { 00372 // call.setResponse(node); 00373 // final Bundle b = new Bundle(); 00374 // b.putParcelable(EXTRA_APICALL, call); 00375 // final Message msg = Message.obtain(null, MSG_RECEIVE_APICALL); 00376 // msg.setData(b); 00377 // try { 00378 // map.get(id).send(msg); 00379 // Log.i(TAG, "Sent updated API call " + call.getName() + " to client."); 00380 // } catch (RemoteException e) { 00381 // Log.e(TAG, "Error sending API response to client: " + e.getMessage(), e); 00382 // map.remove(id); 00383 // } finally { 00384 // // clean up 00385 // map.remove(id); 00386 // calls.remove(id); 00387 // } 00388 // } else { 00389 // Log.w(TAG, "Cannot find client in caller-mapping for " + id + ", dropping response (api call)."); 00390 // } 00391 // } 00392 // } else { 00393 // Log.e(TAG, "Error: Cannot find API call with ID " + id + "."); 00394 // } 00395 // } else { 00396 // // it's a notification. 00397 // final AbstractEvent event = AbstractEvent.parse((ObjectNode) node); 00398 // if (event != null) { 00399 // Log.i(TAG, "Notifying " + clients.size() + " clients."); 00400 // for (int i = clients.size() - 1; i >= 0; i--) { 00401 // try { 00402 // final Bundle b = new Bundle(); 00403 // b.putParcelable(EXTRA_NOTIFICATION, event); 00404 // final Message msg = Message.obtain(null, MSG_RECEIVE_NOTIFICATION); 00405 // msg.setData(b); 00406 // clients.get(i).send(msg); 00407 // 00408 // } catch (RemoteException e) { 00409 // Log.e(TAG, "Cannot send notification to client: " + e.getMessage(), e); 00410 // /* 00411 // * The client is dead. Remove it from the list; we are 00412 // * going through the list from back to front so this is 00413 // * safe to do inside the loop. 00414 // */ 00415 // clients.remove(i); 00416 // // stopSelf(); 00417 // } 00418 // } 00419 // } else { 00420 // Log.i(TAG, "Ignoring unknown notification " + node.get("method").getTextValue() + "."); 00421 // } 00422 // } 00423 // } 00424 // 00425 // /** 00426 // * Sends an error to all clients. 00427 // * @param e Thrown API exception 00428 // * @param id ID of the request 00429 // */ 00430 // private void notifyError(ApiException e, String id) { 00431 // 00432 // // if id is set and callback exists, only send error back to one client. 00433 // if (id != null && mClientMap.containsKey(id)) { 00434 // try { 00435 // final Message msg = Message.obtain(null, MSG_ERROR); 00436 // msg.setData(e.getBundle(getResources())); 00437 // mClientMap.get(id).send(msg); 00438 // Log.i(TAG, "Sent error to client with ID " + id + "."); 00439 // } catch (RemoteException e2) { 00440 // Log.e(TAG, "Cannot send errors to client " + id + ": "+ e.getMessage(), e2); 00441 // mClientMap.remove(id); 00442 // } 00443 // } else { 00444 // // otherwise, send error back to all clients and die. 00445 // for (int i = mClients.size() - 1; i >= 0; i--) { 00446 // final Message msg = Message.obtain(null, MSG_ERROR); 00447 // msg.setData(e.getBundle(getResources())); 00448 // try { 00449 // mClients.get(i).send(msg); 00450 // Log.i(TAG, "Sent error to client " + i + "."); 00451 // } catch (RemoteException e2) { 00452 // Log.e(TAG, "Cannot send errors to client: " + e2.getMessage(), e2); 00453 // /* 00454 // * The client is dead. Remove it from the list; we are going 00455 // * through the list from back to front so this is safe to do 00456 // * inside the loop. 00457 // */ 00458 // mClients.remove(i); 00459 // } 00460 // } 00461 // stopSelf(); 00462 // } 00463 // } 00464 // 00465 // private void notifyStatus(int code, Messenger replyTo) { 00466 // if (replyTo != null) { 00467 // try { 00468 // replyTo.send(Message.obtain(null, code)); 00469 // } catch (RemoteException e) { 00470 // Log.e(TAG, "Could not notify sender of new status: " + e.getMessage(), e); 00471 // } 00472 // } else { 00473 // for (int i = mClients.size() - 1; i >= 0; i--) { 00474 // final Message msg = Message.obtain(null, code); 00475 // try { 00476 // mClients.get(i).send(msg); 00477 // } catch (RemoteException e2) { 00478 // Log.e(TAG, "Could not notify sender of new status: " + e2.getMessage(), e2); 00479 // mClients.remove(i); 00480 // } 00481 // } 00482 // } 00483 // } 00484 // 00485 // 00486 // 00487 // /** 00488 // * Serializes the API request and dumps it on the socket. 00489 // * @param call 00490 // */ 00491 // private void writeSocket(AbstractCall<?> call) { 00492 // final String data = call.getRequest().toString(); 00493 // Log.d(TAG, "Sending data to server."); 00494 // Log.d(TAG, "REQUEST: " + data); 00495 // try { 00496 // mOut.write(data + "\n"); 00497 // mOut.flush(); 00498 // } catch (IOException e) { 00499 // Log.e(TAG, "Error writing to socket: " + e.getMessage(), e); 00500 // notifyError(new ApiException(ApiException.IO_EXCEPTION_WHILE_WRITING, "I/O error while writing: " + e.getMessage(), e), call.getId()); 00501 // } 00502 // } 00503 // 00504 // /** 00505 // * Handler of incoming messages from clients. 00506 // */ 00507 // private class IncomingHandler extends Handler { 00508 // @Override 00509 // public void handleMessage(Message msg) { 00510 // switch (msg.what) { 00511 // case MSG_REGISTER_CLIENT: 00512 // mClients.add(msg.replyTo); 00513 // Log.d(TAG, "Registered new client."); 00514 // if (mCooldownTimer != null) { 00515 // Log.i(TAG, "Aborting cooldown timer."); 00516 // mCooldownTimer.cancel(); 00517 // mCooldownTimer.purge(); 00518 // } 00519 // if (mSocket != null && mSocket.isConnected()) { 00520 // Log.d(TAG, "Directly notifying connected status."); 00521 // notifyStatus(MSG_CONNECTED, msg.replyTo); 00522 // } 00523 // Log.d(TAG, "Done!"); 00524 // break; 00525 // case MSG_UNREGISTER_CLIENT: 00526 // mClients.remove(msg.replyTo); 00527 // Log.d(TAG, "Unregistered client."); 00528 // if (mClients.size() == 0) { 00529 // Log.i(TAG, "No more clients, cooling down service."); 00530 // cooldown(); 00531 // } 00532 // break; 00533 // case MSG_SEND_APICALL: { 00534 // Log.d(TAG, "Sending new API call.."); 00535 // final Bundle data = msg.getData(); 00536 // final AbstractCall<?> call = (AbstractCall<?>)data.getParcelable(EXTRA_APICALL); 00537 // mCalls.put(call.getId(), call); 00538 // mClientMap.put(call.getId(), msg.replyTo); 00539 // if (mOut == null) { 00540 // mPendingCalls.add(call); 00541 // } else { 00542 // writeSocket(call); 00543 // } 00544 // } 00545 // break; 00546 // case MSG_SEND_HANDLED_APICALL: { 00547 // Log.d(TAG, "Sending new handled API call.."); 00548 // final Bundle data = msg.getData(); 00549 // final AbstractCall<?> call = (AbstractCall<?>)data.getParcelable(EXTRA_APICALL); 00550 // final JsonHandler handler = (JsonHandler)data.getParcelable(EXTRA_HANDLER); 00551 // mCalls.put(call.getId(), call); 00552 // mHandlers.put(call.getId(), handler); 00553 // mClientMap.put(call.getId(), msg.replyTo); 00554 // if (mOut == null) { 00555 // Log.d(TAG, "Quering for later."); 00556 // mPendingCalls.add(call); 00557 // } else { 00558 // writeSocket(call); 00559 // } 00560 // } 00561 // break; 00562 // default: 00563 // super.handleMessage(msg); 00564 // } 00565 // } 00566 // } 00567 //}