1 /* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.android.bluetooth.opp; 34 35 import android.content.ContentValues; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.net.Uri; 39 import android.os.Handler; 40 import android.os.Message; 41 import android.os.PowerManager; 42 import android.os.PowerManager.WakeLock; 43 import android.os.SystemClock; 44 import android.util.Log; 45 import android.webkit.MimeTypeMap; 46 47 import com.android.bluetooth.BluetoothMetricsProto; 48 import com.android.bluetooth.BluetoothObexTransport; 49 import com.android.bluetooth.btservice.MetricsLogger; 50 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.io.OutputStream; 55 import java.util.Arrays; 56 57 import javax.obex.HeaderSet; 58 import javax.obex.ObexTransport; 59 import javax.obex.Operation; 60 import javax.obex.ResponseCodes; 61 import javax.obex.ServerRequestHandler; 62 import javax.obex.ServerSession; 63 64 /** 65 * This class runs as an OBEX server 66 */ 67 public class BluetoothOppObexServerSession extends ServerRequestHandler 68 implements BluetoothOppObexSession { 69 70 private static final String TAG = "BtOppObexServer"; 71 private static final boolean D = Constants.DEBUG; 72 private static final boolean V = Constants.VERBOSE; 73 74 private ObexTransport mTransport; 75 76 private Context mContext; 77 78 private Handler mCallback = null; 79 80 /* status when server is blocking for user/auto confirmation */ 81 private boolean mServerBlocking = true; 82 83 /* the current transfer info */ 84 private BluetoothOppShareInfo mInfo; 85 86 /* info id when we insert the record */ 87 private int mLocalShareInfoId; 88 89 private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING; 90 91 private boolean mInterrupted = false; 92 93 private ServerSession mSession; 94 95 private long mTimestamp; 96 97 private BluetoothOppReceiveFileInfo mFileInfo; 98 99 private WakeLock mPartialWakeLock; 100 101 boolean mTimeoutMsgSent = false; 102 103 private BluetoothOppService mBluetoothOppService; 104 105 private int mNumFilesAttemptedToReceive; 106 BluetoothOppObexServerSession(Context context, ObexTransport transport, BluetoothOppService service)107 public BluetoothOppObexServerSession(Context context, ObexTransport transport, 108 BluetoothOppService service) { 109 mContext = context; 110 mTransport = transport; 111 mBluetoothOppService = service; 112 PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 113 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 114 mPartialWakeLock.setReferenceCounted(false); 115 } 116 117 @Override unblock()118 public void unblock() { 119 mServerBlocking = false; 120 } 121 122 /** 123 * Called when connection is accepted from remote, to retrieve the first 124 * Header then wait for user confirmation 125 */ preStart()126 public void preStart() { 127 try { 128 if (D) { 129 Log.d(TAG, "Create ServerSession with transport " + mTransport.toString()); 130 } 131 mSession = new ServerSession(mTransport, this, null); 132 } catch (IOException e) { 133 Log.e(TAG, "Create server session error" + e); 134 } 135 } 136 137 /** 138 * Called from BluetoothOppTransfer to start the "Transfer" 139 */ 140 @Override start(Handler handler, int numShares)141 public void start(Handler handler, int numShares) { 142 if (D) { 143 Log.d(TAG, "Start!"); 144 } 145 mCallback = handler; 146 147 } 148 149 /** 150 * Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise, 151 * server should end by itself. 152 */ 153 @Override stop()154 public void stop() { 155 /* 156 * TODO now we implement in a tough way, just close the socket. 157 * maybe need nice way 158 */ 159 if (D) { 160 Log.d(TAG, "Stop!"); 161 } 162 mInterrupted = true; 163 if (mSession != null) { 164 try { 165 mSession.close(); 166 mTransport.close(); 167 } catch (IOException e) { 168 Log.e(TAG, "close mTransport error" + e); 169 } 170 } 171 mCallback = null; 172 mSession = null; 173 } 174 175 @Override addShare(BluetoothOppShareInfo info)176 public void addShare(BluetoothOppShareInfo info) { 177 if (D) { 178 Log.d(TAG, "addShare for id " + info.mId); 179 } 180 mInfo = info; 181 mFileInfo = processShareInfo(); 182 } 183 184 @Override onPut(Operation op)185 public int onPut(Operation op) { 186 if (D) { 187 Log.d(TAG, "onPut " + op.toString()); 188 } 189 190 /* For multiple objects, reject further objects after the user denies the first one */ 191 if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) { 192 return ResponseCodes.OBEX_HTTP_FORBIDDEN; 193 } 194 195 String destination; 196 if (mTransport instanceof BluetoothObexTransport) { 197 destination = ((BluetoothObexTransport) mTransport).getRemoteAddress(); 198 } else { 199 destination = "FF:FF:FF:00:00:00"; 200 } 201 boolean isWhitelisted = 202 BluetoothOppManager.getInstance(mContext).isWhitelisted(destination); 203 204 HeaderSet request; 205 String name, mimeType; 206 Long length; 207 try { 208 request = op.getReceivedHeader(); 209 if (V) { 210 Constants.logHeader(request); 211 } 212 name = (String) request.getHeader(HeaderSet.NAME); 213 length = (Long) request.getHeader(HeaderSet.LENGTH); 214 mimeType = (String) request.getHeader(HeaderSet.TYPE); 215 } catch (IOException e) { 216 Log.e(TAG, "onPut: getReceivedHeaders error " + e); 217 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 218 } 219 220 if (length == 0) { 221 if (D) { 222 Log.w(TAG, "length is 0, reject the transfer"); 223 } 224 return ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED; 225 } 226 227 if (name == null || name.isEmpty()) { 228 if (D) { 229 Log.w(TAG, "name is null or empty, reject the transfer"); 230 } 231 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 232 } 233 234 // First we look for the mime type in the Android map 235 String extension, type; 236 int dotIndex = name.lastIndexOf("."); 237 if (dotIndex < 0 && mimeType == null) { 238 if (D) { 239 Log.w(TAG, "There is no file extension or mime type, reject the transfer"); 240 } 241 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 242 } else { 243 extension = name.substring(dotIndex + 1).toLowerCase(); 244 MimeTypeMap map = MimeTypeMap.getSingleton(); 245 type = map.getMimeTypeFromExtension(extension); 246 if (V) { 247 Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type); 248 } 249 if (type != null) { 250 mimeType = type; 251 } else { 252 if (mimeType == null) { 253 if (D) { 254 Log.w(TAG, "Can't get mimetype, reject the transfer"); 255 } 256 return ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 257 } 258 } 259 mimeType = mimeType.toLowerCase(); 260 } 261 262 // Reject anything outside the "whitelist" plus unspecified MIME Types. 263 if (mimeType == null || (!isWhitelisted && !Constants.mimeTypeMatches(mimeType, 264 Constants.ACCEPTABLE_SHARE_INBOUND_TYPES))) { 265 if (D) { 266 Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer"); 267 } 268 return ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 269 } 270 271 ContentValues values = new ContentValues(); 272 values.put(BluetoothShare.FILENAME_HINT, name); 273 values.put(BluetoothShare.TOTAL_BYTES, length); 274 values.put(BluetoothShare.MIMETYPE, mimeType); 275 values.put(BluetoothShare.DESTINATION, destination); 276 values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND); 277 values.put(BluetoothShare.TIMESTAMP, mTimestamp); 278 279 // It's not first put if !serverBlocking, so we auto accept it 280 if (!mServerBlocking && (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED 281 || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED)) { 282 values.put(BluetoothShare.USER_CONFIRMATION, 283 BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED); 284 } 285 286 if (isWhitelisted) { 287 values.put(BluetoothShare.USER_CONFIRMATION, 288 BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED); 289 } 290 291 Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values); 292 mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1)); 293 294 if (V) { 295 Log.v(TAG, "insert contentUri: " + contentUri); 296 Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId); 297 } 298 299 synchronized (this) { 300 mPartialWakeLock.acquire(); 301 mServerBlocking = true; 302 try { 303 304 while (mServerBlocking) { 305 wait(1000); 306 if (mCallback != null && !mTimeoutMsgSent) { 307 mCallback.sendMessageDelayed(mCallback.obtainMessage( 308 BluetoothOppObexSession.MSG_CONNECT_TIMEOUT), 309 BluetoothOppObexSession.SESSION_TIMEOUT); 310 mTimeoutMsgSent = true; 311 if (V) { 312 Log.v(TAG, "MSG_CONNECT_TIMEOUT sent"); 313 } 314 } 315 } 316 } catch (InterruptedException e) { 317 if (V) { 318 Log.v(TAG, "Interrupted in onPut blocking"); 319 } 320 } 321 } 322 if (D) { 323 Log.d(TAG, "Server unblocked "); 324 } 325 synchronized (this) { 326 if (mCallback != null && mTimeoutMsgSent) { 327 mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT); 328 } 329 } 330 331 /* we should have mInfo now */ 332 333 /* 334 * TODO check if this mInfo match the one that we insert before server 335 * blocking? just to make sure no error happens 336 */ 337 if (mInfo.mId != mLocalShareInfoId) { 338 Log.e(TAG, "Unexpected error!"); 339 } 340 mAccepted = mInfo.mConfirm; 341 342 if (V) { 343 Log.v(TAG, "after confirm: userAccepted=" + mAccepted); 344 } 345 int status = BluetoothShare.STATUS_SUCCESS; 346 347 int obexResponse = ResponseCodes.OBEX_HTTP_OK; 348 349 if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED 350 || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED 351 || mAccepted == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED) { 352 /* Confirm or auto-confirm */ 353 mNumFilesAttemptedToReceive++; 354 355 if (mFileInfo.mFileName == null) { 356 status = mFileInfo.mStatus; 357 /* TODO need to check if this line is correct */ 358 mInfo.mStatus = mFileInfo.mStatus; 359 Constants.updateShareStatus(mContext, mInfo.mId, status); 360 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 361 362 } 363 364 if (mFileInfo.mFileName != null && mFileInfo.mInsertUri != null) { 365 366 ContentValues updateValues = new ContentValues(); 367 contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 368 updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName); 369 updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING); 370 updateValues.put(BluetoothShare.URI, mFileInfo.mInsertUri.toString()); 371 mContext.getContentResolver().update(contentUri, updateValues, null, null); 372 373 mInfo.mUri = mFileInfo.mInsertUri; 374 status = receiveFile(mFileInfo, op); 375 /* 376 * TODO map status to obex response code 377 */ 378 if (status != BluetoothShare.STATUS_SUCCESS) { 379 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 380 } 381 Constants.updateShareStatus(mContext, mInfo.mId, status); 382 } 383 384 if (status == BluetoothShare.STATUS_SUCCESS) { 385 Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE); 386 msg.obj = mInfo; 387 msg.sendToTarget(); 388 } else { 389 if (mCallback != null) { 390 Message msg = 391 Message.obtain(mCallback, BluetoothOppObexSession.MSG_SESSION_ERROR); 392 mInfo.mStatus = status; 393 msg.obj = mInfo; 394 msg.sendToTarget(); 395 } 396 } 397 } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED 398 || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) { 399 /* user actively deny the inbound transfer */ 400 /* 401 * Note There is a question: what's next if user deny the first obj? 402 * Option 1 :continue prompt for next objects 403 * Option 2 :reject next objects and finish the session 404 * Now we take option 2: 405 */ 406 407 Log.i(TAG, "Rejected incoming request"); 408 if (mFileInfo.mInsertUri != null) { 409 mContext.getContentResolver().delete(mFileInfo.mInsertUri, null, null); 410 } 411 // set status as local cancel 412 status = BluetoothShare.STATUS_CANCELED; 413 Constants.updateShareStatus(mContext, mInfo.mId, status); 414 obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN; 415 416 Message msg = Message.obtain(mCallback); 417 msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED; 418 mInfo.mStatus = status; 419 msg.obj = mInfo; 420 msg.sendToTarget(); 421 } 422 return obexResponse; 423 } 424 receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op)425 private int receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op) { 426 /* 427 * implement receive file 428 */ 429 int status = -1; 430 OutputStream os = null; 431 InputStream is = null; 432 boolean error = false; 433 try { 434 is = op.openInputStream(); 435 } catch (IOException e1) { 436 Log.e(TAG, "Error when openInputStream"); 437 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 438 error = true; 439 } 440 441 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 442 443 if (!error) { 444 ContentValues updateValues = new ContentValues(); 445 updateValues.put(BluetoothShare._DATA, fileInfo.mFileName); 446 mContext.getContentResolver().update(contentUri, updateValues, null, null); 447 } 448 449 long position = 0; 450 long percent; 451 long prevPercent = 0; 452 453 if (!error) { 454 try { 455 os = mContext.getContentResolver().openOutputStream(fileInfo.mInsertUri); 456 } catch (FileNotFoundException e) { 457 Log.e(TAG, "Error when openOutputStream"); 458 error = true; 459 } 460 } 461 462 if (!error) { 463 int outputBufferSize = op.getMaxPacketSize(); 464 byte[] b = new byte[outputBufferSize]; 465 int readLength; 466 long timestamp = 0; 467 long currentTime; 468 long prevTimestamp = SystemClock.elapsedRealtime(); 469 try { 470 while ((!mInterrupted) && (position != fileInfo.mLength)) { 471 472 if (V) { 473 timestamp = SystemClock.elapsedRealtime(); 474 } 475 476 readLength = is.read(b); 477 478 if (readLength == -1) { 479 if (D) { 480 Log.d(TAG, "Receive file reached stream end at position" + position); 481 } 482 break; 483 } 484 485 os.write(b, 0, readLength); 486 position += readLength; 487 percent = position * 100 / fileInfo.mLength; 488 currentTime = SystemClock.elapsedRealtime(); 489 490 if (V) { 491 Log.v(TAG, 492 "Receive file position = " + position + " readLength " + readLength 493 + " bytes took " + (currentTime - timestamp) + " ms"); 494 } 495 496 // Update the Progress Bar only if there is change in percentage 497 // or once per a period to notify NFC of this transfer is still alive 498 if (percent > prevPercent 499 || currentTime - prevTimestamp > Constants.NFC_ALIVE_CHECK_MS) { 500 ContentValues updateValues = new ContentValues(); 501 updateValues.put(BluetoothShare.CURRENT_BYTES, position); 502 mContext.getContentResolver().update(contentUri, updateValues, null, null); 503 prevPercent = percent; 504 prevTimestamp = currentTime; 505 } 506 } 507 } catch (IOException e1) { 508 Log.e(TAG, "Error when receiving file: " + e1); 509 /* OBEX Abort packet received from remote device */ 510 if ("Abort Received".equals(e1.getMessage())) { 511 status = BluetoothShare.STATUS_CANCELED; 512 } else { 513 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 514 } 515 error = true; 516 } 517 } 518 519 if (mInterrupted) { 520 if (D) { 521 Log.d(TAG, "receiving file interrupted by user."); 522 } 523 status = BluetoothShare.STATUS_CANCELED; 524 } else { 525 if (position == fileInfo.mLength) { 526 if (D) { 527 Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName); 528 } 529 status = BluetoothShare.STATUS_SUCCESS; 530 } else { 531 if (D) { 532 Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength); 533 } 534 if (status == -1) { 535 status = BluetoothShare.STATUS_UNKNOWN_ERROR; 536 } 537 } 538 } 539 540 if (os != null) { 541 try { 542 os.flush(); 543 os.close(); 544 } catch (IOException e) { 545 Log.e(TAG, "Error when closing stream after send"); 546 } 547 } 548 BluetoothOppUtility.cancelNotification(mContext); 549 return status; 550 } 551 processShareInfo()552 private BluetoothOppReceiveFileInfo processShareInfo() { 553 if (D) { 554 Log.d(TAG, "processShareInfo() " + mInfo.mId); 555 } 556 BluetoothOppReceiveFileInfo fileInfo = 557 BluetoothOppReceiveFileInfo.generateFileInfo(mContext, mInfo.mId); 558 if (V) { 559 Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:"); 560 Log.v(TAG, "filename :" + fileInfo.mFileName); 561 Log.v(TAG, "length :" + fileInfo.mLength); 562 Log.v(TAG, "status :" + fileInfo.mStatus); 563 } 564 return fileInfo; 565 } 566 567 @Override onConnect(HeaderSet request, HeaderSet reply)568 public int onConnect(HeaderSet request, HeaderSet reply) { 569 570 if (D) { 571 Log.d(TAG, "onConnect"); 572 } 573 if (V) { 574 Constants.logHeader(request); 575 } 576 Long objectCount = null; 577 try { 578 byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET); 579 if (V) { 580 Log.v(TAG, "onConnect(): uuid =" + Arrays.toString(uuid)); 581 } 582 if (uuid != null) { 583 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; 584 } 585 586 objectCount = (Long) request.getHeader(HeaderSet.COUNT); 587 } catch (IOException e) { 588 Log.e(TAG, e.toString()); 589 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 590 } 591 String destination; 592 if (mTransport instanceof BluetoothObexTransport) { 593 destination = ((BluetoothObexTransport) mTransport).getRemoteAddress(); 594 } else { 595 destination = "FF:FF:FF:00:00:00"; 596 } 597 boolean isHandover = BluetoothOppManager.getInstance(mContext).isWhitelisted(destination); 598 if (isHandover) { 599 // Notify the handover requester file transfer has started 600 Intent intent = new Intent(Constants.ACTION_HANDOVER_STARTED); 601 if (objectCount != null) { 602 intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT, objectCount.intValue()); 603 } else { 604 intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT, 605 Constants.COUNT_HEADER_UNAVAILABLE); 606 } 607 intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, destination); 608 mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION); 609 } 610 mTimestamp = System.currentTimeMillis(); 611 mNumFilesAttemptedToReceive = 0; 612 return ResponseCodes.OBEX_HTTP_OK; 613 } 614 615 @Override onDisconnect(HeaderSet req, HeaderSet resp)616 public void onDisconnect(HeaderSet req, HeaderSet resp) { 617 if (D) { 618 Log.d(TAG, "onDisconnect"); 619 } 620 if (mNumFilesAttemptedToReceive > 0) { 621 // Log incoming OPP transfer if more than one file is accepted by user 622 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.OPP); 623 } 624 resp.responseCode = ResponseCodes.OBEX_HTTP_OK; 625 } 626 releaseWakeLocks()627 private synchronized void releaseWakeLocks() { 628 if (mPartialWakeLock.isHeld()) { 629 mPartialWakeLock.release(); 630 } 631 } 632 633 @Override onClose()634 public void onClose() { 635 if (D) { 636 Log.d(TAG, "onClose"); 637 } 638 releaseWakeLocks(); 639 mBluetoothOppService.acceptNewConnections(); 640 BluetoothOppUtility.cancelNotification(mContext); 641 /* onClose could happen even before start() where mCallback is set */ 642 if (mCallback != null) { 643 Message msg = Message.obtain(mCallback); 644 msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE; 645 msg.obj = mInfo; 646 msg.sendToTarget(); 647 } 648 } 649 } 650