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