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.io; 00023 // 00024 //import java.util.ArrayList; 00025 //import java.util.HashMap; 00026 //import java.util.LinkedList; 00027 // 00028 //import org.codehaus.jackson.JsonNode; 00029 //import org.xbmc.android.jsonrpc.api.AbstractCall; 00030 //import org.xbmc.android.jsonrpc.api.AbstractModel; 00031 //import org.xbmc.android.jsonrpc.config.HostConfig; 00032 //import org.xbmc.android.jsonrpc.notification.AbstractEvent; 00033 //import org.xbmc.android.jsonrpc.notification.PlayerEvent; 00034 //import org.xbmc.android.jsonrpc.notification.PlayerObserver; 00035 //import org.xbmc.android.jsonrpc.notification.SystemEvent; 00036 //import org.xbmc.android.jsonrpc.service.ConnectionService; 00037 // 00039 // * Provides simple access to XBMC's JSON-RPC API. 00040 // * <p/> 00041 // * It is used for two things: 00042 // * <ol><li>Query JSON-API via persistent TCP socket or HTTP.</li> 00043 // * <li>Subscribe to notification events from XBMC.</li></ol> 00044 // * 00045 // * TCP socket is used by default. If you want to force HTTP request, use 00046 // * {@link #setPreferHTTP()}, which will use HTTP as transport layer and 00047 // * not use the connection service but {@link JsonApiRequest}. 00048 // * <p/> 00049 // * 00050 // * The TCP connection is managed by {@link ConnectionService}. The manager uses 00051 // * a {@link Messenger} to communicate with the service using a {@link Handler} 00052 // * on both sides (see {@link IncomingHandler}). 00053 // * <p/> 00054 // * 00055 // * <h3>Serialization</h3> 00056 // * Since we're dealing with a service, objects sent to and received by the 00057 // * service must be either native types or {@link Parcelable}. For this reason, 00058 // * our entire JSON-RPC library implements {@link Parcelable}. That includes 00059 // * all classes extending {@link AbstractCall} as well as {@link AbstractModel}. 00060 // * <p/> 00061 // * Once the service receives the API call object, it queries XBMC with the 00062 // * given JSON data and uses the Jackson parser to serialize the response 00063 // * directly into a {@link JsonNode}. Since <tt>JsonNode</tt> is not parcelable, 00064 // * the service directly converts it into our object model using the API call 00065 // * object. The updated API call object is then sent back to 00066 // * <tt>ConnectionManager</tt>. 00067 // * 00068 // * <h3>Synchronization</h3> 00069 // * When syncing the local database we want to avoid the parcelization happening 00070 // * when sending the response back from <tt>ConnectionService</tt> to 00071 // * <tt>ConnectionManager</tt>. Therefore, <tt>call()</tt> can additionally 00072 // * provide a {@link JsonHandler}, which will synchronize the local DB and only 00073 // * respond with a status code instead of the whole response. 00074 // * 00075 // * <h3>Notifications</h3> 00076 // * Every instance of {@link ConnectionManager} appears as a client on the 00077 // * service's side. Upon reception of a notification, the service announces all 00078 // * connected clients. If {@link ConnectionManager} has any registered 00079 // * observers, they will be notified, otherwise the notification is dropped. 00080 // * 00081 // * <h3>Destruction</h3> 00082 // * When an instance of ConnectionManager is not needed anymore, be sure to run 00083 // * {@link #disconnect()} in order to un-bind the service and shut down the TCP 00084 // * connection when it's not needed anymore. Re-using a disconnected instance 00085 // * will re-bind automatically. Note that on error, the service is always 00086 // * disconnected automatically. 00087 // * 00088 // * @author freezy <freezy@xbmc.org> 00089 // */ 00090 //public class ConnectionManager { 00091 // 00092 // private static final String TAG = ConnectionManager.class.getSimpleName(); 00093 // 00094 // private final static String HTTP_PATH = "/jsonrpc"; 00095 // 00096 // /** 00097 // * Reference to context 00098 // */ 00100 // /** 00101 // * True if bound to service, false otherwise. 00102 // */ 00103 // boolean mIsBound; 00104 // /** 00105 // * The reference through which we receive messages from the service 00106 // */ 00108 // /** 00109 // * The reference through which we send messages to the service 00110 // */ 00112 // /** 00113 // * List of observers listening to notifications. 00114 // */ 00115 // private final ArrayList<NotificationObserver> mObservers = new ArrayList<NotificationObserver>(); 00116 // /** 00117 // * List of currently processing API calls with handler. Key is the ID of the API call. 00118 // */ 00119 // private final HashMap<String, HandlerCallback> mHandlerCallbacks = new HashMap<String, HandlerCallback>(); 00120 // /** 00121 // * Since we can't return the de-serialized object from the service, put the 00122 // * response back into the received one and return the received one. 00123 // */ 00124 // private final HashMap<String, CallRequest<?>> mCallRequests = new HashMap<String, CallRequest<?>>(); 00125 // /** 00126 // * When posting request data and the service isn't started yet, we need to 00127 // * reschedule the post until the service is available. This list contains 00128 // * the requests that are to sent upon service startup. 00129 // */ 00130 // private final LinkedList<AbstractCall<?>> mPendingCalls = new LinkedList<AbstractCall<?>>(); 00131 // private final HashMap<String, JsonHandler> mPendingHandlers = new HashMap<String, JsonHandler>(); 00132 // 00133 // /** 00134 // * XBMC host configuration 00135 // */ 00136 // private final HostConfig mHost; 00137 // 00138 // /** 00139 // * Path where data gets posted. 00140 // */ 00141 // private String mHttpPath = HTTP_PATH; 00142 // 00143 // /** 00144 // * If true and HTTP port is set, use HTTP requests instead of the TCP service. 00145 // */ 00146 // private boolean mPreferHTTP = false; 00147 // 00148 // /** 00149 // * Class constructor. 00150 // * @param c Needed if the service needs to be started 00151 // */ 00152 // public ConnectionManager(HostConfig host) { 00154 // mHost = host; 00155 // } 00156 // 00157 // /** 00158 // * Executes a JSON-RPC request with the full result in the callback. 00159 // * @param call Call to execute 00160 // * @param callback 00161 // * @return 00162 // */ 00163 // public <T> ConnectionManager call(final AbstractCall<T> call, final ApiCallback<T> callback) { 00164 // 00165 // if (mPreferHTTP) { 00166 // 00167 // // spawn another thread for this 00168 // new Thread(new Runnable() { 00169 // @Override 00170 // public void run() { 00171 // try { 00172 // // synchronously post, retrieve and parse response. 00173 // call.setResponse(JsonApiRequest.execute(getUrl(), mHost.getUsername(), mHost.getPassword(), call.getRequest())); 00174 // callback.onResponse(call); 00175 // } catch (ApiException e) { 00177 // } 00178 // } 00179 // }).start(); 00180 // 00181 // } else { 00182 // 00183 // // start service if not yet started 00184 // bindService(); 00185 // mCallRequests.put(call.getId(), new CallRequest<T>(call, callback)); 00186 // sendCall(call); 00187 // } 00188 // return this; 00189 // } 00190 // 00191 // /** 00192 // * Executes a JSON-RPC request where the handler is executed at the service 00193 // * and the callback gets a status code only. 00194 // * 00195 // * @param call Call to execute 00196 // * @param handler Handler to treat result 00197 // * @param callback Callback to handle result, can be null. 00198 // * @return 00199 // */ 00200 // public ConnectionManager call(AbstractCall<?> call, JsonHandler handler, HandlerCallback callback) { 00201 // 00202 // // start service if not yet started 00203 // bindService(); 00204 // mHandlerCallbacks.put(call.getId(), callback); 00205 // sendCall(call, handler); 00206 // return this; 00207 // } 00208 // 00209 // /** 00210 // * Adds a new notification observer. 00211 // * @param observer New observer 00212 // * @return Class instance 00213 // */ 00214 // public ConnectionManager registerObserver(NotificationObserver observer) { 00215 // // start service if not yet started 00216 // bindService(); 00217 // mObservers.add(observer); 00218 // return this; 00219 // } 00220 // 00221 // /** 00222 // * Removes a previously added observer. 00223 // * @param observer Observer to remove 00224 // * @return Class instance 00225 // */ 00226 // public ConnectionManager unregisterObserver(NotificationObserver observer) { 00227 // final ArrayList<NotificationObserver> observers = mObservers; 00228 // observers.remove(observer); 00229 // // stop service if no more observers. 00230 // if (observers.isEmpty() && mCallRequests.isEmpty() && mHandlerCallbacks.isEmpty()) { 00231 // unbindService(); 00233 // } else { 00235 // } 00236 // return this; 00237 // } 00238 // 00239 // /** 00240 // * Returns true if HTTP is used instead of a permanent TCP socket. 00241 // * @return True if HTTP is used, false otherwise. 00242 // */ 00243 // public boolean prefersHTTP() { 00244 // return mPreferHTTP; 00245 // } 00246 // 00247 // /** 00248 // * Makes the connection manager use HTTP requests instead of the connection 00249 // * service, which uses a permanent TCP socket. 00250 // */ 00251 // public void setPreferHTTP() { 00252 // mPreferHTTP = true; 00253 // } 00254 // 00255 // /** 00256 // * Binds the connection to the notification service if not yet bound. 00257 // */ 00258 // private void bindService() { 00259 // // start service if no observer and no api calls. 00260 // if (!mIsBound) { 00270 // } 00271 // } 00272 // 00273 // /** 00274 // * Unbinds the connection from the notification service. This is done by 00275 // * notifying the service first and then terminating the connection. 00276 // */ 00277 // private void unbindService() { 00278 // if (mIsBound) { 00296 // } else { 00298 // } 00299 // } 00300 // 00301 // /** 00302 // * Posts a API call to the service. 00303 // * @param apiCall API call 00304 // */ 00305 // private void sendCall(AbstractCall<?> apiCall) { 00306 // if (mService != null) { 00307 // try { 00308 // final Message msg = Message.obtain(null, ConnectionService.MSG_SEND_APICALL); 00309 // final Bundle data = new Bundle(); 00310 // data.putParcelable(ConnectionService.EXTRA_APICALL, apiCall); 00311 // msg.setData(data); 00312 // msg.replyTo = mMessenger; 00313 // mService.send(msg); 00314 // Log.i(TAG, "Posted API call service (with callback)."); 00315 // } catch (RemoteException e) { 00316 // Log.e(TAG, "Error posting message to service: " + e.getMessage(), e); 00317 // } 00318 // } else { 00319 // // service not yet started, saving data: 00320 // Log.i(TAG, "Saving post data for later."); 00321 // mPendingCalls.add(apiCall); 00322 // } 00323 // } 00324 // 00325 // /** 00326 // * Posts a new handled API call to the service. 00327 // * @param apiCall API call 00328 // * @param handler Handler to execute in the service 00329 // */ 00330 // private void sendCall(AbstractCall<?> apiCall, JsonHandler handler) { 00331 // if (mService != null) { 00332 // try { 00333 // final Message msg = Message. obtain(null, ConnectionService.MSG_SEND_HANDLED_APICALL); 00334 // final Bundle data = new Bundle(); 00335 // data.putParcelable(ConnectionService.EXTRA_APICALL, apiCall); 00336 // data.putParcelable(ConnectionService.EXTRA_HANDLER, handler); 00337 // msg.setData(data); 00338 // msg.replyTo = mMessenger; 00339 // mService.send(msg); 00340 // Log.i(TAG, "Posted handled API call service."); 00341 // } catch (RemoteException e) { 00342 // Log.e(TAG, "Error posting message to service: " + e.getMessage(), e); 00343 // } 00344 // } else { 00345 // // service not yet started, saving data: 00346 // Log.i(TAG, "Saving post data for later."); 00347 // mPendingCalls.add(apiCall); 00348 // mPendingHandlers.put(apiCall.getId(), handler); 00349 // } 00350 // } 00351 // 00352 // /** 00353 // * Disconnects from the service. 00354 // * 00355 // * Run this as soon as there are no immediate calls to the API. Running it 00356 // * when there are still requests in progress will cut off the callback (so 00357 // * don't do that). However, notification listener will not be affected. Once 00358 // * you run {@link #disconnect()}, you still can re-use the same object 00359 // * later, since it will be reconnect to the service as soon as it's used. 00360 // */ 00361 // public void disconnect() { 00362 // if (mObservers.isEmpty()) { 00363 // unbindService(); 00364 // } 00365 // } 00366 // 00367 // /** 00368 // * Connection used to communicate with the service. 00369 // */ 00370 // private final ServiceConnection mConnection = new ServiceConnection() { 00371 // public void onServiceConnected(ComponentName className, IBinder service) { 00372 // mService = new Messenger(service); 00373 // Log.i(TAG, "Connected to service."); 00374 // try { 00375 // final Message msg = Message.obtain(null, ConnectionService.MSG_REGISTER_CLIENT); 00376 // msg.replyTo = mMessenger; 00377 // mService.send(msg); 00378 // 00379 // } catch (RemoteException e) { 00380 // Log.e(TAG, "Error registering client: " + e.getMessage(), e); 00381 // // In this case the service has crashed before we could even do 00382 // // anything with it 00383 // } 00384 // // now check if there are lost requests: 00385 // final LinkedList<AbstractCall<?>> calls = mPendingCalls; 00386 // while (!calls.isEmpty()) { 00387 // AbstractCall<?> call = calls.poll(); 00388 // if (mPendingHandlers.containsKey(call.getId())) { 00389 // Log.d(TAG, "Posting pending handled call " + call.getName() + "..."); 00390 // final JsonHandler handler = mPendingHandlers.get(call.getId()); 00391 // sendCall(call, handler); 00392 // mPendingHandlers.remove(call.getId()); 00393 // } else { 00394 // Log.d(TAG, "Posting pending call " + call.getName() + " with callback..."); 00395 // sendCall(call); 00396 // } 00397 // } 00398 // } 00399 // 00400 // public void onServiceDisconnected(ComponentName className) { 00401 // // This is called when the connection with the service has been 00402 // // unexpectedly disconnected - process crashed. 00403 // mService = null; 00404 // Log.i(TAG, "Service disconnected."); 00405 // } 00406 // }; 00407 // 00408 // /** 00409 // * Returns the path of the HTTP request. Default is {@link ConnectionManager#HTTP_PATH}. 00410 // * @return HTTP path 00411 // */ 00412 // public String getHttpPath() { 00413 // return mHttpPath; 00414 // } 00415 // 00416 // /** 00417 // * Sets the path of the HTTP request. Should start with slash and end without. 00418 // * @param httpPath 00419 // */ 00420 // public void setHttpPath(String httpPath) { 00421 // mHttpPath = httpPath; 00422 // } 00423 // 00424 // /** 00425 // * The handler from the receiving service. 00426 // * <p> 00427 // * In here we add the logic of what happens when we get messages from the 00428 // * notification service. 00429 // * 00430 // * @author freezy <freezy@xbmc.org> 00431 // */ 00432 // private class IncomingHandler extends Handler { 00433 // @Override 00434 // public void handleMessage(Message msg) { 00435 // Log.i(TAG, "Got message: " + msg.what); 00436 // final HashMap<String, CallRequest<?>> callrequests = mCallRequests; 00437 // final HashMap<String, HandlerCallback> handlercallbacks = mHandlerCallbacks; 00438 // switch (msg.what) { 00439 // 00440 // // fully updated API call object 00441 // case ConnectionService.MSG_RECEIVE_APICALL: { 00442 // final AbstractCall<?> returnedApiCall = msg.getData().getParcelable(ConnectionService.EXTRA_APICALL); 00443 // if (returnedApiCall != null) { 00444 // if (callrequests.containsKey(returnedApiCall.getId())) { 00445 // final CallRequest<?> callrequest = callrequests.get(returnedApiCall.getId()); 00446 // callrequest.update(returnedApiCall); 00447 // callrequest.respond(); 00448 // callrequests.remove(returnedApiCall.getId()); 00449 // Log.d(TAG, "Callback for " + returnedApiCall.getName() + " sent back to caller."); 00450 // } else { 00451 // Log.w(TAG, "Unknown ID " + returnedApiCall.getId() + " for " + returnedApiCall.getName() + ", dropping."); 00452 // } 00453 // } else { 00454 // Log.e(TAG, "Error retrieving API call object from bundle."); 00455 // } 00456 // break; 00457 // } 00458 // 00459 // // status code after handled api call 00460 // case ConnectionService.MSG_RECEIVE_HANDLED_APICALL: { 00461 // final Bundle b = msg.getData(); 00462 // final String id = b.getString(ConnectionService.EXTRA_CALLID); 00463 // if (handlercallbacks.containsKey(id)) { 00464 // if (handlercallbacks.get(id) != null) { 00465 // handlercallbacks.get(id).onFinish(); 00466 // handlercallbacks.remove(id); 00467 // } 00468 // } else { 00469 // Log.w(TAG, "Unknown ID " + id + " for handled callback, not notifying caller."); 00470 // } 00471 // break; 00472 // } 00473 // 00474 // // notification 00475 // case ConnectionService.MSG_RECEIVE_NOTIFICATION: { 00476 // final Bundle b = msg.getData(); 00477 // final AbstractEvent notification = b.getParcelable(ConnectionService.EXTRA_NOTIFICATION); 00478 // final ArrayList<NotificationObserver> observers = mObservers; 00479 // for (NotificationObserver observer : observers) { 00480 // switch (notification.getId()) { 00481 // case PlayerEvent.Play.ID: 00482 // observer.getPlayerObserver().onPlay((PlayerEvent.Play)notification); 00483 // break; 00484 // case PlayerEvent.Pause.ID: 00485 // observer.getPlayerObserver().onPause((PlayerEvent.Pause)notification); 00486 // break; 00487 // case PlayerEvent.Stop.ID: 00488 // observer.getPlayerObserver().onStop((PlayerEvent.Stop)notification); 00489 // break; 00490 // case PlayerEvent.SpeedChanged.ID: 00491 // observer.getPlayerObserver().onSpeedChanged((PlayerEvent.SpeedChanged)notification); 00492 // break; 00493 // case PlayerEvent.Seek.ID: 00494 // observer.getPlayerObserver().onSeek((PlayerEvent.Seek)notification); 00495 // break; 00496 // case SystemEvent.Quit.ID: 00497 // case SystemEvent.Restart.ID: 00498 // case SystemEvent.Wake.ID: 00499 // case SystemEvent.LowBattery.ID: 00500 // default: 00501 // break; 00502 // } 00503 // } 00504 // break; 00505 // } 00506 // 00507 // // service started connecting to socket 00508 // case ConnectionService.MSG_CONNECTING: { 00509 // // we don't care for this right now 00510 // break; 00511 // } 00512 // 00513 // // service is connected to socket 00514 // case ConnectionService.MSG_CONNECTED: { 00515 // final ArrayList<NotificationObserver> observers = mObservers; 00516 // for (NotificationObserver observer : observers) { 00517 // observer.onConnected(); 00518 // } 00519 // break; 00520 // } 00521 // 00522 // // shit happened 00523 // case ConnectionService.MSG_ERROR: { 00524 // final Bundle b = msg.getData(); 00525 // final int code = b.getInt(ApiException.EXTRA_ERROR_CODE); 00526 // final String message = b.getString(ApiException.EXTRA_ERROR_MESSAGE); 00527 // final String hint = b.getString(ApiException.EXTRA_ERROR_HINT); 00528 // final String id = b.getString(ConnectionService.EXTRA_CALLID); 00529 // 00530 // final HashMap<String, HandlerCallback> handleCallbacks = mHandlerCallbacks; 00531 // final HashMap<String, CallRequest<?>> callRequests = mCallRequests; 00532 // 00533 // if (id != null && handleCallbacks.containsKey(id)) { 00534 // // if ID given and handler call back, announce to handler callback. 00535 // Log.e(TAG, "Error, notifying one handler callback."); 00536 // if (handleCallbacks.get(id) != null) { 00537 // handleCallbacks.get(id).onError(message, hint); 00538 // } 00539 // } else if (id != null && callRequests.containsKey(id)) { 00540 // // if ID given and api call back, announce error. 00541 // Log.e(TAG, "Error, notifying one API callback."); 00542 // callRequests.get(id).error(code, message, hint); 00543 // } else { 00544 // // otherwise, announce to all clients (callbacks, api callbacks and observers). 00545 // Log.e(TAG, "Error, notifying everybody."); 00546 // for (HandlerCallback handlerCallback : handleCallbacks.values()) { 00547 // if (handlerCallback != null) { 00548 // handlerCallback.onError(message, hint); 00549 // } 00550 // } 00551 // handleCallbacks.clear(); 00552 // 00553 // for (CallRequest<?> callreq : callRequests.values()) { 00554 // callreq.error(code, message, hint); 00555 // } 00556 // callRequests.clear(); 00557 // 00558 // final ArrayList<NotificationObserver> observers = mObservers; 00559 // for (NotificationObserver observer : observers) { 00560 // observer.onError(code, message, hint); 00561 // } 00562 // observers.clear(); 00563 // unbindService(); 00564 // } 00565 // break; 00566 // } 00567 // default: 00568 // super.handleMessage(msg); 00569 // } 00570 // } 00571 // } 00572 // 00573 // 00574 // /** 00575 // * Returns the URL of XBMC to connect to. 00576 // * 00577 // * The URL already contains the JSON-RPC prefix, e.g.: 00578 // * <code>http://192.168.0.100:8080/jsonrpc</code> 00579 // * @return URL of JSON-RPC via HTTP 00580 // */ 00581 // private String getUrl() { 00582 // return "http://" + mHost.getAddress() + ":" + mHost.getHttpPort()+ mHttpPath; 00583 // } 00584 // 00585 // /** 00586 // * A call request bundles an API call and its callback of the same type. 00587 // * 00588 // * @author freezy <freezy@xbmc.org> 00589 // */ 00590 // private static class CallRequest<T> { 00591 // private final AbstractCall<T> mCall; 00592 // private final ApiCallback<T> mCallback; 00593 // public CallRequest(AbstractCall<T> call, ApiCallback<T> callback) { 00594 // this.mCall = call; 00595 // this.mCallback = callback; 00596 // } 00597 // public void update(AbstractCall<?> call) { 00598 // mCall.copyResponse(call); 00599 // } 00600 // public void respond() { 00601 // mCallback.onResponse(mCall); 00602 // } 00603 // public void error(int code, String message, String hint) { 00604 // mCallback.onError(code, message, hint); 00605 // } 00606 // } 00607 // 00608 // /** 00609 // * Observer interface that handles arriving notifications. 00610 // * @author freezy <freezy@xbmc.org> 00611 // */ 00612 // public static interface NotificationObserver { 00613 // /** 00614 // * Handle an arriving notification in here. 00615 // */ 00616 // public PlayerObserver getPlayerObserver(); 00617 // 00618 // /** 00619 // * The service is connected to JSON-RPC's TCP socket. 00620 // * <p/> 00621 // * If the service was already connected, this will be sent immediately 00622 // * after registering the client. 00623 // */ 00624 // public void onConnected(); 00625 // 00626 // /** 00627 // * An error has occurred which resulted in the termination of the 00628 // * connection. 00629 // * @param code Error code, see constants at {@link ApiException}. 00630 // * @param message Translated error message 00631 // * @param hint Translated hint what the problem could be 00632 // */ 00633 // public void onError(int code, String message, String hint); 00634 // } 00635 // 00636 // /** 00637 // * When providing a {@link JsonHandler} to an API call, this interface 00638 // * will inform the caller when processing has finished. 00639 // * 00640 // * @author freezy <freezy@xbmc.org> 00641 // */ 00642 // public static interface HandlerCallback { 00643 // /** 00644 // * Processing has successfully finished. 00645 // * <p> 00646 // * Don't forget to run {@link ConnectionManager#disconnect()} in here 00647 // * if you don't immediately need to run another call. 00648 // */ 00649 // public void onFinish(); 00650 // /** 00651 // * Processing has failed. 00652 // * <p> 00653 // * Note that the service has been automatically disconnected. 00654 // * @param message Translated error message 00655 // * @param hint Translated hint 00656 // */ 00657 // public void onError(String message, String hint); 00658 // } 00659 // 00660 //}