Home | History | Annotate | Download | only in mapservice
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.android.auto.mapservice;
     18 
     19 import android.bluetooth.BluetoothDevice;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.content.BroadcastReceiver;
     22 import android.content.ComponentName;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.IntentFilter;
     26 import android.content.ServiceConnection;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.Looper;
     30 import android.os.RemoteException;
     31 import android.util.Log;
     32 
     33 import java.lang.ref.WeakReference;
     34 import java.util.List;
     35 
     36 public final class BluetoothMapManager {
     37     public static final String CALLBACK_MISMATCH = "CALLBACK_MISMATCH";
     38 
     39     /**
     40      * Connection State(s) for the service bound to this Manager.
     41      * The connection state manage if the Manager is connected to Service:
     42      * DISCONNECTED: Manager cannot execute calls on service currently because the client either
     43      * never called connect() on the manager OR the device is disconnected. In the later case the
     44      * client will have to eventually call connect() again.
     45      * CONNECTING: This state persists from calling onBind on the service, until we have
     46      * successfully received onConnect from the service.
     47      * CONNECTED: The service is successfully connected and ready to receive commands.
     48      */
     49     private static final int DISCONNECTED = 0;
     50     private static final int SUSPENDED = 1;
     51     private static final int CONNECTING = 2;
     52     private static final int CONNECTED = 3;
     53 
     54     // Error codes returned via the onError call.
     55 
     56     // On connection suspended the Manager will callback with onConnect when the service is
     57     // restarted and connected by android binder.
     58     public static final int ERROR_CONNECT_SUSPENDED = 0;
     59     // Connection between the service (backed by this Manager) and the remote device has failed.
     60     // It can be due to variety of reasons such as obex transport failure or the device going out
     61     // of range. The client will need to either call connect() again. In cases where the device
     62     // goes out of range, calling connect agian will lead to this error being throw again but if
     63     // it was a transient failure due to obex transport or other binder issue, then this call will
     64     // succeed.
     65     public static final int ERROR_CONNECT_FAILED = 1;
     66 
     67     // String representation of operations.
     68     private static final String OP_NONE = "";
     69     private static final String OP_PUSH_MESSAGE = "pushMessage";
     70     private static final String OP_GET_MESSAGE = "getMessage";
     71     private static final String OP_GET_MESSAGES_LISTING = "getMessagesListing";
     72     private static final String OP_ENABLE_NOTIFICATIONS = "enableNotifications";
     73 
     74     private static final boolean DBG = true;
     75     private static final String TAG = "BluetoothMapManager";
     76     private final Context mContext;
     77     private final ConnectionCallbacks mCallbacks;
     78     private final BluetoothDevice mDevice;
     79     // We have a handler to make sure that all code modifying/accessing the final non-final
     80     // objects are serialized. This is done by ensuring the following:
     81     // a) All calls done by the client (using this manager) should be on the main thread.
     82     // b) All calls done by the manager not-on main thread (binder threads) should be posted back
     83     // to main thread using this handler.
     84     private final Handler mHandler = new Handler();
     85 
     86     private IBluetoothMapService mServiceBinder;
     87     private IBluetoothMapServiceCallbacks mServiceCallbacks;
     88     private BluetoothMapServiceConnection mServiceConnection;
     89     private int mConnectionState = DISCONNECTED;
     90     private String mOpInflight = OP_NONE;
     91 
     92     public BluetoothMapManager(
     93         Context context, BluetoothDevice device, ConnectionCallbacks callbacks) {
     94         if (device == null) {
     95           throw new IllegalArgumentException("Device cannot be null.");
     96         }
     97         if (callbacks == null) {
     98             throw new IllegalArgumentException(TAG + ": Callbacks cannot be null!");
     99         }
    100 
    101         if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
    102             throw new IllegalStateException(
    103                 "Client needs to call the manager from the main UI thread.");
    104         }
    105 
    106         mDevice = device;
    107         mContext = context;
    108         mCallbacks = callbacks;
    109     }
    110 
    111     /**
    112      * Defines the callback interface that clients using the Manager should implement in order to
    113      * receive callbacks and notification for changes happening to either the binder connection
    114      * between the Manager and Service or MAP profile changes.
    115      */
    116     public static abstract class ConnectionCallbacks {
    117         /**
    118          * Called when connection has been established successfully with the service and the
    119          * service itself is successfully connected to MAP profile.
    120          * See connect().
    121          */
    122         public abstract void onConnected();
    123 
    124         /**
    125          * Called when the Manager is no longer able to execute commands on the service.
    126          * Client who holds this manager should consider all in-flight commands sent till now as
    127          * cancelled. For permanent failures such as device going out of range and not coming back
    128          * the client should call connect() again too continue working otherwise
    129          * when the Manager does connect back it will call onConnected() (see above).
    130          */
    131         public abstract void onError(int errorCode);
    132 
    133         /**
    134          * Callen when notification status has been adjusted.
    135          * See enableNotifications().
    136          */
    137         public abstract void onEnableNotifications();
    138 
    139         /**
    140          * Called when the message has been queued for sending.
    141          * The argument always contains a "valid" handle.
    142          * See pushMessage().
    143          */
    144         public abstract void onPushMessage(String handle);
    145 
    146         /**
    147          * Called when the message is fetched.
    148          * See getMessage().
    149          */
    150         public abstract void onGetMessage(BluetoothMapMessage msg);
    151 
    152         /**
    153          * Called when the messages listing is retrieved.
    154          * See getMessagesListing().
    155          */
    156         public abstract void onGetMessagesListing(List<BluetoothMapMessagesListing> msgsListing);
    157 
    158         /**
    159          * Called when an event has occured.
    160          * See BluetoothMapEventReport for a description of what an event may look like.
    161          */
    162         public abstract void onEvent(BluetoothMapEventReport eventReport);
    163 
    164     }
    165 
    166     /**
    167      * Bind to the service and register the callback passed in the constructor.
    168      */
    169     public boolean connect() {
    170         checkMainThread();
    171 
    172         if (mConnectionState != DISCONNECTED && mConnectionState != SUSPENDED) {
    173             Log.w(TAG, "Not in disconnected state, connection will eventually resume: " +
    174                 mConnectionState);
    175             return true;
    176         }
    177         mConnectionState = CONNECTING;
    178         mServiceConnection = new BluetoothMapServiceConnection();
    179 
    180         boolean bound = false;
    181         ComponentName cName =
    182             new ComponentName(
    183                 "com.google.android.auto.mapservice",
    184                 "com.google.android.auto.mapservice.BluetoothMapService");
    185         final Intent intent = new Intent();
    186         intent.setComponent(cName);
    187         try {
    188             bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    189         } catch (Exception ex) {
    190             Log.e(TAG, "Failed binding to service." + ex);
    191             dumpState();
    192             return false;
    193         }
    194 
    195         if (!bound) {
    196             forceCloseConnection();
    197             dumpState();
    198             return false;
    199         }
    200 
    201         dumpState();
    202         return true;
    203     }
    204 
    205     /**
    206      * Unregister the callbacks and unbind from the service.
    207      */
    208     public void disconnect() {
    209         checkMainThread();
    210 
    211         if (DBG) {
    212             Log.d(TAG, "Calling IBluetoothMapService.disconnect ...");
    213         }
    214 
    215         // In case the manager is already disconnected, we don't need to do anything more here.
    216         if (mServiceBinder != null) {
    217             try {
    218                 mServiceBinder.disconnect(mServiceCallbacks);
    219             } catch (RemoteException ex) {
    220                 Log.w(TAG, "RemoteException during disconnect for " + mServiceConnection);
    221             } catch (IllegalStateException ex) {
    222                 sendError(ex);
    223             }
    224         }
    225         forceCloseConnection();
    226         dumpState();
    227     }
    228 
    229     /**
    230      * Enable notifications.
    231      */
    232     public void enableNotifications(boolean status) {
    233         checkMainThread();
    234 
    235         if (DBG) {
    236             Log.d(TAG, "Calling IBluetoothMapService.enableNotifications ..." + status);
    237         }
    238 
    239         if (mConnectionState != CONNECTED) {
    240             if (DBG) {
    241                 Log.d(TAG, "Not connected to service.");
    242             }
    243             throw new IllegalStateException(
    244                 "Service is not connected, either connect() is not called or a disconnect " +
    245                 "event is not handled correctly.");
    246         }
    247 
    248         if (!mOpInflight.equals(OP_NONE)) {
    249             throw new IllegalStateException(
    250                 TAG + "Operation already in flight: " + mOpInflight +
    251                 ". Please wait for an appropriate callback from your previous operation.");
    252         }
    253 
    254         try {
    255             mServiceBinder.enableNotifications(mServiceCallbacks, status);
    256         } catch (RemoteException ex) {
    257             // If we have disconnected then the client will get a ERROR_CONNECT_SUSPENDED.
    258             Log.e(TAG, "", ex);
    259             return;
    260         } catch (IllegalStateException ex) {
    261             sendError(ex);
    262         }
    263         mOpInflight = OP_ENABLE_NOTIFICATIONS;
    264     }
    265 
    266     /**
    267      * Push a message.
    268      */
    269     public void pushMessage(BluetoothMapMessage msg) {
    270         checkMainThread();
    271 
    272         if (mConnectionState != CONNECTED) {
    273             throw new IllegalStateException(
    274                 "Service is not connected, either connect() is not called or a disconnect " +
    275                 "event is not handled correctly.");
    276         }
    277 
    278         if (!mOpInflight.equals(OP_NONE)) {
    279             throw new IllegalStateException(
    280                 TAG + "Operation already in flight: " + mOpInflight +
    281                 ". Please wait for an appropriate callback from your previous operation.");
    282         }
    283 
    284         try {
    285             mServiceBinder.pushMessage(mServiceCallbacks, msg);
    286         } catch (RemoteException ex) {
    287             // If we have disconnected then the client will get a ERROR_CONNECT_SUSPENDED.
    288             Log.e(TAG, "", ex);
    289             return;
    290         } catch (IllegalStateException ex) {
    291             sendError(ex);
    292         }
    293         mOpInflight = OP_PUSH_MESSAGE;
    294     }
    295 
    296     /**
    297      * Get a message by its handle.
    298      */
    299     public void getMessage(String handle) {
    300         checkMainThread();
    301 
    302         if (mConnectionState != CONNECTED) {
    303             throw new IllegalStateException(
    304                 "Service is not connected, either connect() is not called or a disconnect " +
    305                 "event is not handled correctly.");
    306         }
    307 
    308         if (!mOpInflight.equals(OP_NONE)) {
    309             throw new IllegalStateException(
    310                 TAG + "Operation already in flight: " + mOpInflight +
    311                 ". Please wait for an appropriate callback from your previous operation.");
    312         }
    313 
    314         try {
    315             mServiceBinder.getMessage(mServiceCallbacks, handle);
    316         } catch (RemoteException ex) {
    317             // If we have disconnected then the client will get a ERROR_CONNECT_SUSPENDED.
    318             Log.e(TAG, "", ex);
    319             return;
    320         } catch (IllegalStateException ex) {
    321             sendError(ex);
    322         }
    323         mOpInflight = OP_GET_MESSAGE;
    324     }
    325 
    326     public void getMessagesListing(String folder) {
    327         // If count is not specified the cap is put by the Bluetooth spec, we pass a large number
    328         // to use the cap provided by spec implementation on MAS server.
    329         getMessagesListing(folder, 65535, 0);
    330     }
    331     public void getMessagesListing(String folder, int count) {
    332         getMessagesListing(folder, count, 0);
    333     }
    334     public void getMessagesListing(String folder, int count, int offset) {
    335         checkMainThread();
    336 
    337         if (mConnectionState != CONNECTED) {
    338             throw new IllegalStateException(
    339                 "Service is not connected, either connect() is not called or a disconnect " +
    340                 "event is not handled correctly.");
    341         }
    342 
    343         if (!mOpInflight.equals(OP_NONE)) {
    344             throw new IllegalStateException(
    345                 TAG + "Operation already in flight: " + mOpInflight +
    346                 ". Please wait for an appropriate callback from your previous operation.");
    347         }
    348 
    349         try {
    350             mServiceBinder.getMessagesListing(mServiceCallbacks, folder, count, offset);
    351         } catch (RemoteException ex) {
    352             // If we have disconnected then the client will get a ERROR_CONNECT_SUSPENDED.
    353             Log.e(TAG, "", ex);
    354             return;
    355         } catch (IllegalStateException ex) {
    356             sendError(ex);
    357         }
    358         mOpInflight = OP_GET_MESSAGES_LISTING;
    359     }
    360 
    361     /**
    362      * Checks if the current thread is main thread.
    363      *
    364      * Throws an IllegalStateException otherwise.
    365      */
    366     void checkMainThread() {
    367         if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
    368             throw new IllegalStateException(
    369                 "Manager APIs should be called only from main thread.");
    370         }
    371     }
    372 
    373     /**
    374      * Implements the callbacks when changes in the binder connection of Manager (this) and the
    375      * BluetoothMapService occur.
    376      * For a list of possible states that the binder connection can exist, see DISCONNECTED,
    377      * CONNECTING and CONNECTED states above.
    378      */
    379     private class BluetoothMapServiceConnection implements ServiceConnection {
    380         /**
    381          * Called when the Manager connects to service via the binder */
    382         @Override
    383         public void onServiceConnected(ComponentName name, IBinder binder) {
    384             // onServiceConnected can be called either because we called onBind and in which case
    385             // connection state should already be CONNECTING, or it could be called because the
    386             // service came back up in which case we need to set it to CONNECTING (from
    387             // DISCONNECTED).
    388             if (mConnectionState == CONNECTED) {
    389                 throw new IllegalStateException(
    390                     "Cannot be in connected state while (re)connecting.");
    391             }
    392             // We may either be in CONNECTING or DISCONNECTED state here. Its safe to set to
    393             // CONNECTING in any scenario.
    394             mConnectionState = CONNECTING;
    395 
    396             if (DBG) {
    397                 Log.d(TAG, "BluetoothMapServiceConnection.onServiceConnected name=" +
    398                     name + " binder=" + binder);
    399             }
    400 
    401             // Save the binder for future calls to service.
    402             mServiceBinder = IBluetoothMapService.Stub.asInterface(binder);
    403 
    404             // Register the callbacks to the service.
    405             mServiceCallbacks = new ServiceCallbacks(BluetoothMapManager.this);
    406 
    407             try {
    408                 if (DBG) {
    409                   Log.d(TAG, "ServiceCallbacks.connect ...");
    410                 }
    411                 boolean status = mServiceBinder.connect(mServiceCallbacks, mDevice);
    412                 if (DBG && !status) {
    413                     Log.d(TAG, "Failed to connect to service after binding.");
    414                 }
    415             } catch (RemoteException ex) {
    416                 Log.d(TAG, "connect failed with RemoteException.");
    417             }
    418         }
    419 
    420         /**
    421          * Called when the service is disconnected from the manager due to binder failure.
    422          */
    423         @Override
    424         public void onServiceDisconnected(ComponentName name) {
    425             if (DBG) {
    426                 Log.d(TAG, "BluetoothMapServiceConnection.onServiceDisconnected name=" + name
    427                         + " this=" + this + "mServiceConnection=" + mServiceConnection);
    428             }
    429 
    430             mConnectionState = SUSPENDED;
    431 
    432             mServiceBinder = null;
    433             mServiceCallbacks = null;
    434             mOpInflight = OP_NONE;
    435             mCallbacks.onError(ERROR_CONNECT_SUSPENDED);
    436         }
    437     }
    438 
    439     /**
    440      * Implements the AIDL interface which is called by BluetoothMapService either in reply to any
    441      * of the commands issued to it via the service binder or when there's a new notification that
    442      * service has to push to Manager.
    443      */
    444     private static class ServiceCallbacks extends IBluetoothMapServiceCallbacks.Stub {
    445         private WeakReference<BluetoothMapManager> mMngr;
    446 
    447         public ServiceCallbacks(BluetoothMapManager manager) {
    448             mMngr = new WeakReference<BluetoothMapManager>(manager);
    449         }
    450 
    451         /**
    452          * Called when the service is successfully connected to a remote device and is capable to
    453          * execute the MAP profile.
    454          */
    455         @Override
    456         public void onConnect() {
    457             if (DBG) {
    458                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onConnect() called.");
    459             }
    460 
    461             // Client may clean up the manager before the service responds, we may have a race
    462             // if the manager instance has disappeared, in which case we can return silently.
    463             BluetoothMapManager mgr = mMngr.get();
    464             if (mgr == null) return;
    465 
    466             mgr.onServiceConnected();
    467         }
    468 
    469         /**
    470          * Called when the service is not connected to a remote device and cannot execute the MAP
    471          * profile.
    472          */
    473         @Override
    474         public void onConnectFailed() {
    475             if (DBG) {
    476                Log.d(TAG, "IBluetoothMapServiceCallbacks.onConnectionFailed() called.");
    477             }
    478 
    479             // Client may clean up the manager before the service responds, we may have a race if
    480             // the manager instance has disappeared, in which case we can return silently.
    481             BluetoothMapManager mgr = mMngr.get();
    482             if (mgr == null) return;
    483 
    484            mgr.onServiceConnectionFailed();
    485         }
    486 
    487         @Override
    488         public void onEnableNotifications() {
    489             if (DBG) {
    490                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onEnableNotifications() called.");
    491             }
    492 
    493             // Client may clean up the manager before the service responds, we may have a race if
    494             // the manager instance has disappeared, in which case we can return silently.
    495             BluetoothMapManager mgr = mMngr.get();
    496             if (mgr == null) return;
    497 
    498             mgr.onEnableNotifications();
    499         }
    500 
    501         @Override
    502         public void onPushMessage(String handle) {
    503             if (DBG) {
    504                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onPushMessage() called with " + handle);
    505             }
    506 
    507             // Client may clean up the manager before the service responds, we may have a race if
    508             // the manager instance has disappeared, in which case we can return silently.
    509             BluetoothMapManager mgr = mMngr.get();
    510             if (mgr == null) return;
    511 
    512             mgr.onPushMessage(handle);
    513         }
    514 
    515         @Override
    516         public void onGetMessage(BluetoothMapMessage msg) {
    517             if (DBG) {
    518                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onGetMessage() called with " + msg);
    519             }
    520 
    521             // Client may clean up the manager before the service responds, we may have a race if
    522             // the manager instance has disappeared, in which case we can return silently.
    523             BluetoothMapManager mgr = mMngr.get();
    524             if (mgr == null) return;
    525 
    526             mgr.onGetMessage(msg);
    527         }
    528 
    529         @Override
    530         public void onGetMessagesListing(List<BluetoothMapMessagesListing> msgsListing) {
    531             if (DBG) {
    532                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onGetMessagesListing() called with " +
    533                     msgsListing);
    534             }
    535 
    536             // Client may clean up the manager before the service responds, we may have a race if
    537             // the manager instance has disappeared, in which case we can return silently.
    538             BluetoothMapManager mgr = mMngr.get();
    539             if (mgr == null) return;
    540 
    541             mgr.onGetMessagesListing(msgsListing);
    542         }
    543 
    544         @Override
    545         public void onEvent(BluetoothMapEventReport eventReport) {
    546              if (DBG) {
    547                 Log.d(TAG, "IBluetoothMapServiceCallbacks.onEvent() called with " + eventReport);
    548              }
    549 
    550              BluetoothMapManager mngr = mMngr.get();
    551             // Client may clean up the manager before the service responds, we may have a race if
    552             // the manager instance has disappeared, in which case we can return silently.
    553             BluetoothMapManager mgr = mMngr.get();
    554             if (mgr == null) return;
    555 
    556             mgr.onEvent(eventReport);
    557         }
    558     };
    559 
    560     private void onServiceConnected() {
    561         mHandler.post(new Runnable() {
    562             @Override
    563             public void run() {
    564                 mConnectionState = CONNECTED;
    565                 mCallbacks.onConnected();
    566                 dumpState();
    567             }
    568         });
    569     }
    570 
    571     private void onServiceConnectionFailed() {
    572         mHandler.post(new Runnable() {
    573             @Override
    574             public void run() {
    575                 forceCloseConnection();
    576                 mCallbacks.onError(ERROR_CONNECT_FAILED);
    577                 dumpState();
    578             }
    579         });
    580     }
    581 
    582     private void onEnableNotifications() {
    583         mHandler.post(new Runnable() {
    584             @Override
    585             public void run() {
    586                 if (!mOpInflight.equals(OP_ENABLE_NOTIFICATIONS)) {
    587                     throw new IllegalStateException(
    588                         TAG + " Expected Inflight op: " + OP_ENABLE_NOTIFICATIONS +
    589                         " actual op: " + mOpInflight);
    590                 }
    591                 mOpInflight = OP_NONE;
    592                 mCallbacks.onEnableNotifications();
    593             }
    594         });
    595     }
    596 
    597     private void onPushMessage(final String handle) {
    598         mHandler.post(new Runnable() {
    599             @Override
    600             public void run() {
    601                 if (!mOpInflight.equals(OP_PUSH_MESSAGE)) {
    602                     throw new IllegalStateException(
    603                         TAG + " Expected Inflight op: " + OP_PUSH_MESSAGE +
    604                         " actual op: " + mOpInflight);
    605                 }
    606 
    607                 if (handle == null || handle.equals("")) {
    608                     Log.e(TAG, "Empty handle, the service may have been disconnected.");
    609                 }
    610                 mOpInflight = OP_NONE;
    611                 mCallbacks.onPushMessage(handle);
    612             }
    613         });
    614     }
    615 
    616     private void onGetMessage(final BluetoothMapMessage msg) {
    617         mHandler.post(new Runnable() {
    618             @Override
    619             public void run() {
    620                 if (!mOpInflight.equals(OP_GET_MESSAGE)) {
    621                     throw new IllegalStateException(
    622                         TAG + " Expected inflight op: " + OP_GET_MESSAGE +
    623                         " actual op: " + mOpInflight);
    624                 }
    625                 mOpInflight = OP_NONE;
    626                 mCallbacks.onGetMessage(msg);
    627             }
    628         });
    629     }
    630 
    631     private void onGetMessagesListing(final List<BluetoothMapMessagesListing> msgsListing) {
    632         mHandler.post(new Runnable() {
    633             @Override
    634             public void run() {
    635                 if (!mOpInflight.equals(OP_GET_MESSAGES_LISTING)) {
    636                     throw new IllegalStateException(
    637                         TAG + " Expected inflight op: " + OP_GET_MESSAGES_LISTING +
    638                         " actual op: " + mOpInflight);
    639                 }
    640                 mOpInflight = OP_NONE;
    641                 mCallbacks.onGetMessagesListing(msgsListing);
    642             }
    643         });
    644     }
    645 
    646     private void onEvent(final BluetoothMapEventReport eventReport) {
    647         mHandler.post(new Runnable() {
    648             @Override
    649             public void run() {
    650                 mCallbacks.onEvent(eventReport);
    651             }
    652         });
    653     }
    654 
    655     private void forceCloseConnection() {
    656         if (mConnectionState == DISCONNECTED) {
    657             Log.e(TAG, "Connection already closed.");
    658             return;
    659         }
    660         mConnectionState = DISCONNECTED;
    661 
    662         if (mServiceConnection != null) {
    663             mContext.unbindService(mServiceConnection);
    664         }
    665         mServiceConnection = null;
    666         mServiceCallbacks = null;
    667         mServiceBinder = null;
    668         // Even if there is an inflight message, we will never hear from the callback now.
    669         mOpInflight = OP_NONE;
    670     }
    671 
    672     private void sendError(IllegalStateException ex) {
    673         if (ex.getMessage().equals(CALLBACK_MISMATCH)) {
    674             throw new IllegalStateException(
    675                 "Client tried to call with an unregistered callback. This can happen if either " +
    676                 "client never called connect() or if it got disconnected and GCed by service but " +
    677                 "forgot to call connect(). Check your connection state and reconnect.");
    678         } else {
    679             throw new IllegalArgumentException(TAG + " unknown exception: " + ex.toString());
    680         }
    681     }
    682 
    683     // Log the state of Manager. Useful for debugging.
    684     private void dumpState() {
    685         if (!DBG) {
    686             return;
    687         }
    688         Log.d(TAG, "dumpState(). Connection State: " + mConnectionState);
    689     }
    690 }
    691 
    692