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 
BluetoothMapManager( Context context, BluetoothDevice device, ConnectionCallbacks callbacks)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          */
onConnected()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          */
onError(int errorCode)131         public abstract void onError(int errorCode);
132 
133         /**
134          * Callen when notification status has been adjusted.
135          * See enableNotifications().
136          */
onEnableNotifications()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          */
onPushMessage(String handle)144         public abstract void onPushMessage(String handle);
145 
146         /**
147          * Called when the message is fetched.
148          * See getMessage().
149          */
onGetMessage(BluetoothMapMessage msg)150         public abstract void onGetMessage(BluetoothMapMessage msg);
151 
152         /**
153          * Called when the messages listing is retrieved.
154          * See getMessagesListing().
155          */
onGetMessagesListing(List<BluetoothMapMessagesListing> msgsListing)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          */
onEvent(BluetoothMapEventReport eventReport)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      */
connect()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      */
disconnect()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      */
enableNotifications(boolean status)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      */
pushMessage(BluetoothMapMessage msg)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      */
getMessage(String handle)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 
getMessagesListing(String folder)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     }
getMessagesListing(String folder, int count)331     public void getMessagesListing(String folder, int count) {
332         getMessagesListing(folder, count, 0);
333     }
getMessagesListing(String folder, int count, int offset)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      */
checkMainThread()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
onServiceConnected(ComponentName name, IBinder binder)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
onServiceDisconnected(ComponentName name)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 
ServiceCallbacks(BluetoothMapManager manager)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
onConnect()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
onConnectFailed()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
onEnableNotifications()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
onPushMessage(String handle)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
onGetMessage(BluetoothMapMessage msg)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
onGetMessagesListing(List<BluetoothMapMessagesListing> msgsListing)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
onEvent(BluetoothMapEventReport eventReport)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 
onServiceConnected()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 
onServiceConnectionFailed()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 
onEnableNotifications()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 
onPushMessage(final String handle)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 
onGetMessage(final BluetoothMapMessage msg)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 
onGetMessagesListing(final List<BluetoothMapMessagesListing> msgsListing)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 
onEvent(final BluetoothMapEventReport eventReport)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 
forceCloseConnection()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 
sendError(IllegalStateException ex)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.
dumpState()684     private void dumpState() {
685         if (!DBG) {
686             return;
687         }
688         Log.d(TAG, "dumpState(). Connection State: " + mConnectionState);
689     }
690 }
691 
692