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