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