• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.android.bluetooth.pbapclient;
18 
19 import static android.Manifest.permission.BLUETOOTH_CONNECT;
20 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
21 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
22 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
23 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
24 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;
25 
26 import static java.util.Objects.requireNonNull;
27 
28 import android.annotation.RequiresPermission;
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothProfile;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.Process;
36 import android.util.Log;
37 
38 import com.android.bluetooth.ObexAppParameters;
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.obex.ClientSession;
41 import com.android.obex.HeaderSet;
42 import com.android.obex.ResponseCodes;
43 
44 import java.io.IOException;
45 import java.util.concurrent.atomic.AtomicInteger;
46 
47 /**
48  * Bluetooth/pbapclient/PbapClientConnectionHandler is responsible for connecting, disconnecting and
49  * downloading contacts from the PBAP PSE when commanded. It receives all direction from the
50  * controlling state machine.
51  */
52 class PbapClientObexClient {
53     private static final String TAG = PbapClientObexClient.class.getSimpleName();
54 
55     static final int MSG_CONNECT = 1;
56     static final int MSG_DISCONNECT = 2;
57     static final int MSG_REQUEST = 3;
58 
59     static final int L2CAP_INVALID_PSM = -1;
60     static final int RFCOMM_INVALID_CHANNEL_ID = -1;
61 
62     // The following constants are pulled from the Bluetooth Phone Book Access Profile specification
63     // 1.1
64     private static final byte[] BLUETOOTH_UUID_PBAP_CLIENT =
65             new byte[] {
66                 0x79,
67                 0x61,
68                 0x35,
69                 (byte) 0xf0,
70                 (byte) 0xf0,
71                 (byte) 0xc5,
72                 0x11,
73                 (byte) 0xd8,
74                 0x09,
75                 0x66,
76                 0x08,
77                 0x00,
78                 0x20,
79                 0x0c,
80                 (byte) 0x9a,
81                 0x66
82             };
83 
84     private static final int PBAP_FEATURES_EXCLUDED = -1;
85 
86     public static final int TRANSPORT_NONE = -1;
87     public static final int TRANSPORT_RFCOMM = 0;
88     public static final int TRANSPORT_L2CAP = 1;
89 
90     private final BluetoothDevice mDevice;
91     private final int mLocalSupportedFeatures;
92     private int mState = STATE_DISCONNECTED;
93     private final AtomicInteger mPsm = new AtomicInteger(L2CAP_INVALID_PSM);
94     private final AtomicInteger mChannelId = new AtomicInteger(RFCOMM_INVALID_CHANNEL_ID);
95 
96     private final Handler mHandler;
97     private final HandlerThread mThread;
98 
99     private PbapClientSocket mSocket; // Wraps a BluetoothSocket, for testability
100     private ClientSession mObexSession;
101     private PbapClientObexAuthenticator mAuth = null;
102 
103     /** Callback object used to be notified of when a request has been completed. */
104     interface Callback {
105 
106         /*
107          * Detailed State Diagram:
108          *                                  +------------------------------+
109          *                                  V                              |
110          *                +---------- DISCONNECTED -----------+            |
111          *                |            ^       ^              |            |
112          *                V            |       |              V            |
113          *    RFCOMM_CONNECTING        |       |          L2CAP_CONNECTING |
114          *    (CONNECTING)             |       |              (CONNECTING) |
115          *     |                       |       |                        |  |
116          *     |      RFCOMM_DISCONNECTING   L2CAP_DISCONNECTING        |  |
117          *     |      (DISCONNECTING)  ^       ^ (DISCONNECTING)        |  |
118          *     V                       |       |                        V  |
119          *  RFCOMM_CONNECTED -----+    |       |    +----- L2CAP_CONNECTED |
120          *  (CONNECTING)          |    |       |    |         (CONNECTING) |
121          *                        |    |       |    |                      |
122          *                        V    |       |    V                      |
123          *                        TRANSPORT_CONNECTED <-+                  |
124          *                          (DIS/CONNECTING)    |                  |
125          *                                 |            |                  |
126          *                                 V            |                  |
127          *               ABORT       OBEX_CONNECTING    |                  |
128          *                ^  \         (CONNECTING)     |                  |
129          *                |   \______      |            |                  |
130          *                |          V     V           /-\                 |
131          *     PROCESS_REQUEST <------ CONNECTED -----+ | +----------------+
132          *                                 |            |
133          *                                 V            |
134          *                          OBEX_DISCONNECTING -+
135          *                            (DISCONNECTING)
136          *
137          * The above is exactly what's going on under the hood, but is not how
138          * we report outward.
139          *
140          * All the transport specific flavored states are wrapped into single
141          * CONNECTING/DISCONNECTING states when reporting outwards, depending
142          * on whether we're trying to establish or bring down the connection.
143          * The connection is assumed fully connected when the OBEX session is
144          * connected.
145          */
146         /**
147          * Notify of a connection state change in the client
148          *
149          * @param oldState The old state of the client
150          * @param newState The new state of the client
151          */
onConnectionStateChanged(int oldState, int newState)152         void onConnectionStateChanged(int oldState, int newState);
153 
154         /**
155          * Notify of a result of a phonebook size request
156          *
157          * @param responseCode The status of the request
158          * @param phonebook The phonebook this update is concerning
159          * @param metadata The metadata object, containing size, DB identifier and any version
160          *     counters relateding to the phonebook
161          */
onGetPhonebookMetadataComplete( int responseCode, String phonebook, PbapPhonebookMetadata metadata)162         void onGetPhonebookMetadataComplete(
163                 int responseCode, String phonebook, PbapPhonebookMetadata metadata);
164 
165         /**
166          * Notify of the result of a phonebook download request
167          *
168          * @param responseCode The status of the request
169          * @param phonebook The phonebook this update is concerning
170          * @param contacts The list of entries downloaded as an object
171          */
onPhonebookContactsDownloaded( int responseCode, String phonebook, PbapPhonebook contacts)172         void onPhonebookContactsDownloaded(
173                 int responseCode, String phonebook, PbapPhonebook contacts);
174     }
175 
176     private final Callback mCallback;
177 
178     /**
179      * Constructs a PbapClientObexClient object
180      *
181      * @param device The device this client should connect to
182      * @param supportedFeatures Our local device's supported features
183      * @param callback A callback object so you can receive updates on completed requests
184      */
PbapClientObexClient(BluetoothDevice device, int supportedFeatures, Callback callback)185     PbapClientObexClient(BluetoothDevice device, int supportedFeatures, Callback callback) {
186         this(device, supportedFeatures, callback, null);
187     }
188 
189     @VisibleForTesting
PbapClientObexClient( BluetoothDevice device, int supportedFeatures, Callback callback, Looper looper)190     PbapClientObexClient(
191             BluetoothDevice device, int supportedFeatures, Callback callback, Looper looper) {
192         mDevice = requireNonNull(device);
193         mCallback = requireNonNull(callback);
194         mAuth = new PbapClientObexAuthenticator();
195         mLocalSupportedFeatures = supportedFeatures;
196 
197         // Allow for injection of test looper
198         if (looper == null) {
199             mThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
200             mThread.start();
201             looper = mThread.getLooper();
202         } else {
203             mThread = null;
204         }
205 
206         mHandler = new PbapClientObexClientHandler(looper);
207     }
208 
209     /**
210      * Get the current connection state of this PBAP Client OBEX client
211      *
212      * <p>This is thread safe to use
213      */
getConnectionState()214     public synchronized int getConnectionState() {
215         return mState;
216     }
217 
218     /**
219      * Set the connection state of this client and notify the callback object of any new states
220      *
221      * <p>This is thread safe to use
222      */
setConnectionState(int newState)223     private void setConnectionState(int newState) {
224         int oldState = -1;
225         synchronized (this) {
226             oldState = mState;
227             mState = newState;
228         }
229         if (oldState != newState) {
230             info("Connection state changed, old=" + oldState + ", new=" + mState);
231             mCallback.onConnectionStateChanged(oldState, mState);
232         }
233     }
234 
235     /**
236      * Determines if this client is connected to the server
237      *
238      * @return True if connected, False otherwise
239      */
isConnected()240     public boolean isConnected() {
241         return getConnectionState() == STATE_CONNECTED;
242     }
243 
244     /**
245      * Connect to the remove device's PBAP server
246      *
247      * <p>This function connects using the L2CAP transport and a provided PSM
248      *
249      * @param psm The L2CAP PSM to connect on
250      */
connectL2cap(int psm)251     public void connectL2cap(int psm) {
252         info("connectL2cap(psm=" + psm + ")");
253         connect(TRANSPORT_L2CAP, psm);
254     }
255 
256     /**
257      * Connect to the remove device's PBAP server
258      *
259      * @param channel The RFCOMM channel ID to connect over
260      */
connectRfcomm(int channel)261     public void connectRfcomm(int channel) {
262         info("connectRfcomm(channel=" + channel + ")");
263         connect(TRANSPORT_RFCOMM, channel);
264     }
265 
266     /**
267      * Connect to the remove device's PBAP server
268      *
269      * @param transport The transport id, Transport.L2CAP or Transport.RFCOMM
270      * @param psmOrChannel The L2CAP PSM or RFCOMM channel id to be used
271      */
connect(int transport, int psmOrChannel)272     private void connect(int transport, int psmOrChannel) {
273         info(
274                 "connect(transport="
275                         + transportToString(transport)
276                         + ", channel/psm="
277                         + psmOrChannel
278                         + ")");
279         mHandler.obtainMessage(MSG_CONNECT, transport, psmOrChannel).sendToTarget();
280     }
281 
282     /**
283      * Get the transport type of this OBEX Client
284      *
285      * @return The transport id, TRANSPORT_L2CAP, TRANSPORT_RFCOMM, or TRANSPORT_NONE
286      */
getTransportType()287     public int getTransportType() {
288         if (getL2capPsm() != L2CAP_INVALID_PSM) {
289             return TRANSPORT_L2CAP;
290         }
291 
292         if (getRfcommChannelId() != RFCOMM_INVALID_CHANNEL_ID) {
293             return TRANSPORT_RFCOMM;
294         }
295 
296         return TRANSPORT_NONE;
297     }
298 
299     /**
300      * Get the L2CAP PSM of this OBEX Client
301      *
302      * @return The L2CAP PSM of this OBEX Client, or L2CAP_INVALID_PSM
303      */
getL2capPsm()304     public int getL2capPsm() {
305         return mPsm.get();
306     }
307 
308     /**
309      * Get the RFCOMM channel id of this OBEX Client
310      *
311      * @return The RFCOMM channel id of this OBEX Client, or RFCOMM_INVALID_CHANNEL_ID
312      */
getRfcommChannelId()313     public int getRfcommChannelId() {
314         return mChannelId.get();
315     }
316 
317     /** Enqueue a request to download the size of a phonebook */
requestPhonebookMetadata(String phonebook, PbapApplicationParameters params)318     public void requestPhonebookMetadata(String phonebook, PbapApplicationParameters params) {
319         RequestPullPhonebookMetadata request = new RequestPullPhonebookMetadata(phonebook, params);
320         mHandler.obtainMessage(MSG_REQUEST, request).sendToTarget();
321     }
322 
323     /** Enqueue a request to download the contents of a phonebook */
requestDownloadPhonebook(String phonebook, PbapApplicationParameters params)324     public void requestDownloadPhonebook(String phonebook, PbapApplicationParameters params) {
325         RequestPullPhonebook request = new RequestPullPhonebook(phonebook, params);
326         mHandler.obtainMessage(MSG_REQUEST, request).sendToTarget();
327     }
328 
329     /**
330      * Enqueue a request to disconnect from the remote device.
331      *
332      * <p>This request will be processed before any further download requests
333      */
disconnect()334     public void disconnect() {
335         info("disconnect: Enqueue disconnect");
336 
337         // Post to front of queue to disconnect immediately in front of any queued downloads
338         mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_DISCONNECT));
339 
340         // Quit the handler thread. All future sendMessage() calls will fail and return false
341         // Any ongoing request qill be finished, disconnect will be processed, and all other
342         // messages will be removed
343         if (mThread != null) {
344             mThread.quitSafely();
345         }
346     }
347 
348     /**
349      * Close this connection immediately by interrupting any ongoing operations and closing the
350      * transport.
351      *
352      * <p>No lifecycle events will be sent. This OBEX Client object will be immediately made
353      * unusable
354      */
close()355     public void close() {
356         info("close: disconnect immediately");
357         mHandler.getLooper().getThread().interrupt();
358         closeSocket(mSocket);
359         if (mThread != null) {
360             mThread.quit();
361         }
362         setConnectionState(STATE_DISCONNECTED);
363     }
364 
365     /** Handles this PBAP Client OBEX Client's requests */
366     private class PbapClientObexClientHandler extends Handler {
367 
PbapClientObexClientHandler(Looper looper)368         PbapClientObexClientHandler(Looper looper) {
369             super(looper);
370         }
371 
372         @Override
handleMessage(Message msg)373         public void handleMessage(Message msg) {
374             debug("Handling Message, type=" + messageToString(msg.what));
375             switch (msg.what) {
376                 case MSG_CONNECT:
377                     if (getConnectionState() != STATE_DISCONNECTED) {
378                         warn("Cannot connect, device not disconnected");
379                         return;
380                     }
381 
382                     // To establish a connection, first open a socket and then create an OBEX
383                     // session. The socket can use either the RFCOMM or L2CAP transport, depending
384                     // on the capabilities of the server. The callee will have checked the SDP
385                     // record and called connect() with the appropriate parameters
386                     int transport = (int) msg.arg1;
387                     int psmOrChannel = msg.arg2;
388 
389                     debug(
390                             "Request connect, transport="
391                                     + transportToString(transport)
392                                     + ", psm/channel="
393                                     + psmOrChannel
394                                     + ", features="
395                                     + mLocalSupportedFeatures);
396 
397                     if (transport == TRANSPORT_L2CAP) {
398                         mPsm.set(psmOrChannel);
399                     } else if (transport == TRANSPORT_RFCOMM) {
400                         mChannelId.set(psmOrChannel);
401                     } else {
402                         error(
403                                 "Unrecognized transport, type='"
404                                         + transportToString(transport)
405                                         + "'");
406                         return;
407                     }
408 
409                     setConnectionState(STATE_CONNECTING);
410 
411                     mSocket = connectSocket(transport, psmOrChannel);
412                     if (mSocket == null) {
413                         mPsm.set(L2CAP_INVALID_PSM);
414                         mChannelId.set(RFCOMM_INVALID_CHANNEL_ID);
415                         setConnectionState(STATE_DISCONNECTED);
416                         return;
417                     }
418 
419                     mObexSession = connectObex(mSocket, mLocalSupportedFeatures);
420                     if (mObexSession == null) {
421                         closeSocket(mSocket);
422                         mSocket = null;
423                         mPsm.set(L2CAP_INVALID_PSM);
424                         mChannelId.set(RFCOMM_INVALID_CHANNEL_ID);
425                         setConnectionState(STATE_DISCONNECTED);
426                         return;
427                     }
428 
429                     setConnectionState(STATE_CONNECTED);
430                     break;
431 
432                 case MSG_DISCONNECT:
433                     removeCallbacksAndMessages(null);
434 
435                     if (getConnectionState() != STATE_CONNECTED) {
436                         warn("Cannot disconnect, device not connected");
437                         return;
438                     }
439 
440                     setConnectionState(STATE_DISCONNECTING);
441 
442                     // To disconnect, first bring down the OBEX session, then bring down the
443                     // underlying transport/socket. If there are any errors while bringing down the
444                     // OBEX session, log them, but move on to the transport anyways. Notify the
445                     // callee so they can clean up the data this client has. Remove any pending
446                     // messages, as we don't want this object to work after and we're going to quit
447                     // safely
448                     disconnectObex(mObexSession);
449                     mObexSession = null;
450 
451                     closeSocket(mSocket);
452                     mSocket = null;
453                     mPsm.set(L2CAP_INVALID_PSM);
454                     mChannelId.set(RFCOMM_INVALID_CHANNEL_ID);
455 
456                     setConnectionState(STATE_DISCONNECTED);
457                     break;
458 
459                 case MSG_REQUEST:
460                     if (isConnected()) {
461                         executeRequest((PbapClientRequest) msg.obj, mObexSession);
462                     } else {
463                         warn("Cannot issue request. Not connected");
464                     }
465                     break;
466 
467                 default:
468                     warn("Received unexpected message, id=" + messageToString(msg.what));
469             }
470         }
471     }
472 
473     /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
474      * channel, or RFCOMM default channel. */
475     @RequiresPermission(
476             allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
477             conditional = true)
connectSocket(int transport, int channelOrPsm)478     private PbapClientSocket connectSocket(int transport, int channelOrPsm) {
479         debug(
480                 "Connect socket, transport="
481                         + transportToString(transport)
482                         + ", channelOrPsm="
483                         + channelOrPsm);
484         PbapClientSocket socket;
485         try {
486             if (transport == TRANSPORT_L2CAP) {
487                 socket = PbapClientSocket.getL2capSocketForDevice(mDevice, channelOrPsm);
488             } else if (transport == TRANSPORT_RFCOMM) {
489                 socket = PbapClientSocket.getRfcommSocketForDevice(mDevice, channelOrPsm);
490             } else {
491                 error("Failed to create socket, unknown transport requested");
492                 return null;
493             }
494 
495             if (socket != null) {
496                 socket.connect();
497                 return socket;
498             } else {
499                 error("Failed to create socket");
500             }
501         } catch (IOException e) {
502             error("Exception while connecting transport", e);
503         }
504         return null;
505     }
506 
507     /**
508      * Connect an OBEX session over the already connected socket.
509      *
510      * <p>First establish an OBEX Transport abstraction, then establish a Bluetooth Authenticator
511      * and finally issue the connect call
512      */
connectObex(PbapClientSocket socket, int supportedFeatures)513     private ClientSession connectObex(PbapClientSocket socket, int supportedFeatures) {
514         debug("Connect OBEX session, socket=" + socket + ", features=" + supportedFeatures);
515         try {
516             PbapClientObexTransport transport = new PbapClientObexTransport(socket);
517             ClientSession obexSession = new ClientSession(transport);
518 
519             obexSession.setAuthenticator(mAuth);
520 
521             HeaderSet headers = new HeaderSet();
522             headers.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_PBAP_CLIENT);
523 
524             if (supportedFeatures != PBAP_FEATURES_EXCLUDED) {
525                 ObexAppParameters oap = new ObexAppParameters();
526                 oap.add(PbapApplicationParameters.OAP_PBAP_SUPPORTED_FEATURES, supportedFeatures);
527                 oap.addToHeaderSet(headers);
528             }
529 
530             HeaderSet responseHeaders = obexSession.connect(headers);
531 
532             info(
533                     "Connection request response received, response code="
534                             + responseHeaders.getResponseCode());
535             if (responseHeaders.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
536                 return obexSession;
537             }
538         } catch (IOException | NullPointerException | IllegalArgumentException e) {
539             // Will get NPE if a null mSocket is passed to BluetoothObexTransport.
540             // mSocket can be set to null if an abort() --> closeSocket() was called between
541             // the calls to connectSocket() and connectObexSession().
542             error("Error while connecting OBEX", e);
543             closeSocket(socket);
544         }
545         return null;
546     }
547 
executeRequest(PbapClientRequest request, ClientSession obexSession)548     private void executeRequest(PbapClientRequest request, ClientSession obexSession) {
549         debug("executeRequest(request=" + request + ")");
550         if (!isConnected()) {
551             error("Cannot execute request " + request.toString() + ", we're not connected");
552             notifyCaller(request);
553             return;
554         }
555 
556         try {
557             request.execute(obexSession);
558             notifyCaller(request);
559         } catch (IOException e) {
560             error("Request failed: " + request.toString());
561             notifyCaller(request);
562             disconnect();
563         }
564     }
565 
notifyCaller(PbapClientRequest request)566     private void notifyCaller(PbapClientRequest request) {
567         int type = request.getType();
568         int responseCode = request.getResponseCode();
569         String phonebook = null;
570 
571         debug("Notifying caller of request result - " + request.toString());
572         switch (type) {
573             case PbapClientRequest.TYPE_PULL_PHONEBOOK_METADATA:
574                 phonebook = ((RequestPullPhonebookMetadata) request).getPhonebook();
575                 PbapPhonebookMetadata metadata =
576                         ((RequestPullPhonebookMetadata) request).getMetadata();
577                 mCallback.onGetPhonebookMetadataComplete(responseCode, phonebook, metadata);
578                 break;
579 
580             case PbapClientRequest.TYPE_PULL_PHONEBOOK:
581                 phonebook = ((RequestPullPhonebook) request).getPhonebook();
582                 PbapPhonebook contacts = ((RequestPullPhonebook) request).getContacts();
583                 mCallback.onPhonebookContactsDownloaded(responseCode, phonebook, contacts);
584                 break;
585         }
586     }
587 
disconnectObex(ClientSession obexSession)588     private void disconnectObex(ClientSession obexSession) {
589         debug("Disconnect OBEX, session=" + obexSession);
590         try {
591             obexSession.disconnect(null);
592         } catch (IOException e) {
593             error("Exception when disconnecting the PBAP Client OBEX session", e);
594         }
595 
596         try {
597             obexSession.close();
598         } catch (IOException e) {
599             error("Exception when closing the PBAP Client OBEX connection", e);
600         }
601     }
602 
closeSocket(PbapClientSocket socket)603     private boolean closeSocket(PbapClientSocket socket) {
604         debug("Disconnect socket transport");
605         try {
606             if (socket != null) {
607                 socket.close();
608                 return true;
609             }
610         } catch (IOException e) {
611             error("Error when closing socket", e);
612         }
613         return false;
614     }
615 
616     @Override
toString()617     public String toString() {
618         StringBuilder sb = new StringBuilder();
619         sb.append("<").append(TAG);
620         sb.append(" device=").append(mDevice);
621         sb.append(" state=").append(BluetoothProfile.getConnectionStateName(getConnectionState()));
622 
623         int transport = getTransportType();
624         sb.append(" transport=").append(transportToString(transport));
625         if (getTransportType() == TRANSPORT_L2CAP) {
626             sb.append(" psm=").append(getL2capPsm());
627         } else if (transport == TRANSPORT_RFCOMM) {
628             sb.append(" channel=").append(getRfcommChannelId());
629         }
630 
631         sb.append(">");
632         return sb.toString();
633     }
634 
transportToString(int transport)635     public static String transportToString(int transport) {
636         switch (transport) {
637             case TRANSPORT_NONE:
638                 return "TRANSPORT_NONE";
639             case TRANSPORT_RFCOMM:
640                 return "TRANSPORT_RFCOMM";
641             case TRANSPORT_L2CAP:
642                 return "TRANSPORT_L2CAP";
643             default:
644                 return "TRANSPORT_RESERVED (" + transport + ")";
645         }
646     }
647 
messageToString(int msg)648     private static String messageToString(int msg) {
649         switch (msg) {
650             case MSG_CONNECT:
651                 return "MSG_CONNECT";
652             case MSG_DISCONNECT:
653                 return "MSG_DISCONNECT";
654             case MSG_REQUEST:
655                 return "MSG_REQUEST";
656             default:
657                 return "MSG_RESERVED (" + msg + ")";
658         }
659     }
660 
debug(String message)661     private void debug(String message) {
662         Log.d(TAG, "[" + mDevice + "] " + message);
663     }
664 
info(String message)665     private void info(String message) {
666         Log.i(TAG, "[" + mDevice + "] " + message);
667     }
668 
warn(String message)669     private void warn(String message) {
670         Log.w(TAG, "[" + mDevice + "] " + message);
671     }
672 
error(String message)673     private void error(String message) {
674         Log.e(TAG, "[" + mDevice + "] " + message);
675     }
676 
error(String message, Exception e)677     private void error(String message, Exception e) {
678         Log.e(TAG, "[" + mDevice + "] " + message, e);
679     }
680 }
681