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