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