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