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.content.ContentValues; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.net.Uri; 45 import android.os.Handler; 46 import android.os.Message; 47 import android.os.PowerManager; 48 import android.os.PowerManager.WakeLock; 49 import android.util.Log; 50 import android.webkit.MimeTypeMap; 51 52 import javax.obex.HeaderSet; 53 import javax.obex.ObexTransport; 54 import javax.obex.Operation; 55 import javax.obex.ResponseCodes; 56 import javax.obex.ServerRequestHandler; 57 import javax.obex.ServerSession; 58 59 /** 60 * This class runs as an OBEX server 61 */ 62 public class BluetoothOppObexServerSession extends ServerRequestHandler implements 63 BluetoothOppObexSession { 64 65 private static final String TAG = "BtOppObexServer"; 66 private static final boolean D = Constants.DEBUG; 67 private static final boolean V = Constants.VERBOSE; 68 69 private ObexTransport mTransport; 70 71 private Context mContext; 72 73 private Handler mCallback = null; 74 75 /* status when server is blocking for user/auto confirmation */ 76 private boolean mServerBlocking = true; 77 78 /* the current transfer info */ 79 private BluetoothOppShareInfo mInfo; 80 81 /* info id when we insert the record */ 82 private int mLocalShareInfoId; 83 84 private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING; 85 86 private boolean mInterrupted = false; 87 88 private ServerSession mSession; 89 90 private long mTimestamp; 91 92 private BluetoothOppReceiveFileInfo mFileInfo; 93 94 private WakeLock mWakeLock; 95 96 private WakeLock mPartialWakeLock; 97 98 boolean mTimeoutMsgSent = false; 99 BluetoothOppObexServerSession(Context context, ObexTransport transport)100 public BluetoothOppObexServerSession(Context context, ObexTransport transport) { 101 mContext = context; 102 mTransport = transport; 103 PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); 104 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP 105 | PowerManager.ON_AFTER_RELEASE, TAG); 106 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 107 } 108 unblock()109 public void unblock() { 110 mServerBlocking = false; 111 } 112 113 /** 114 * Called when connection is accepted from remote, to retrieve the first 115 * Header then wait for user confirmation 116 */ preStart()117 public void preStart() { 118 if (D) Log.d(TAG, "acquire full WakeLock"); 119 mWakeLock.acquire(); 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 */ start(Handler handler)131 public void start(Handler handler) { 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 */ stop()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 addShare(BluetoothOppShareInfo info)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 onPut(Operation op)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 try { 184 boolean pre_reject = false; 185 request = op.getReceivedHeader(); 186 if (V) Constants.logHeader(request); 187 name = (String)request.getHeader(HeaderSet.NAME); 188 length = (Long)request.getHeader(HeaderSet.LENGTH); 189 mimeType = (String)request.getHeader(HeaderSet.TYPE); 190 191 if (length == 0) { 192 if (D) Log.w(TAG, "length is 0, reject the transfer"); 193 pre_reject = true; 194 obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED; 195 } 196 197 if (name == null || name.equals("")) { 198 if (D) Log.w(TAG, "name is null or empty, reject the transfer"); 199 pre_reject = true; 200 obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST; 201 } 202 203 if (!pre_reject) { 204 /* first we look for Mimetype in Android map */ 205 String extension, type; 206 int dotIndex = name.lastIndexOf("."); 207 if (dotIndex < 0) { 208 if (D) Log.w(TAG, "There is no file extension, reject the transfer"); 209 pre_reject = true; 210 obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST; 211 } else { 212 extension = name.substring(dotIndex + 1).toLowerCase(); 213 MimeTypeMap map = MimeTypeMap.getSingleton(); 214 type = map.getMimeTypeFromExtension(extension); 215 if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type); 216 if (type != null) { 217 mimeType = type; 218 219 } else { 220 if (mimeType == null) { 221 if (D) Log.w(TAG, "Can't get mimetype, reject the transfer"); 222 pre_reject = true; 223 obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 224 } 225 } 226 if (mimeType != null) { 227 mimeType = mimeType.toLowerCase(); 228 } 229 } 230 } 231 232 // Reject policy: anything outside the "white list" plus unspecified 233 // MIME Types. 234 if (!pre_reject 235 && (mimeType == null || (!Constants.mimeTypeMatches(mimeType, 236 Constants.ACCEPTABLE_SHARE_INBOUND_TYPES)))) { 237 if (D) Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer"); 238 pre_reject = true; 239 obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 240 } 241 242 if (pre_reject && obexResponse != ResponseCodes.OBEX_HTTP_OK) { 243 // some bad implemented client won't send disconnect 244 return obexResponse; 245 } 246 247 } catch (IOException e) { 248 Log.e(TAG, "get getReceivedHeaders error " + e); 249 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 250 } 251 252 ContentValues values = new ContentValues(); 253 254 values.put(BluetoothShare.FILENAME_HINT, name); 255 values.put(BluetoothShare.TOTAL_BYTES, length.intValue()); 256 values.put(BluetoothShare.MIMETYPE, mimeType); 257 258 if (mTransport instanceof BluetoothOppRfcommTransport) { 259 String a = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress(); 260 values.put(BluetoothShare.DESTINATION, a); 261 } else { 262 values.put(BluetoothShare.DESTINATION, "FF:FF:FF:00:00:00"); 263 } 264 265 values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND); 266 values.put(BluetoothShare.TIMESTAMP, mTimestamp); 267 268 boolean needConfirm = true; 269 /** It's not first put if !serverBlocking, so we auto accept it */ 270 if (!mServerBlocking) { 271 values.put(BluetoothShare.USER_CONFIRMATION, 272 BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED); 273 needConfirm = false; 274 } 275 276 Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values); 277 mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1)); 278 279 if (needConfirm) { 280 Intent in = new Intent(BluetoothShare.INCOMING_FILE_CONFIRMATION_REQUEST_ACTION); 281 in.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 282 mContext.sendBroadcast(in); 283 } 284 285 if (V) Log.v(TAG, "insert contentUri: " + contentUri); 286 if (V) Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId); 287 288 if (V) Log.v(TAG, "acquire partial WakeLock"); 289 290 291 synchronized (this) { 292 if (mWakeLock.isHeld()) { 293 mPartialWakeLock.acquire(); 294 mWakeLock.release(); 295 } 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 /* Confirm or auto-confirm */ 337 338 if (mFileInfo.mFileName == null) { 339 status = mFileInfo.mStatus; 340 /* TODO need to check if this line is correct */ 341 mInfo.mStatus = mFileInfo.mStatus; 342 Constants.updateShareStatus(mContext, mInfo.mId, status); 343 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 344 345 } 346 347 if (mFileInfo.mFileName != null) { 348 349 ContentValues updateValues = new ContentValues(); 350 contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 351 updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName); 352 updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING); 353 mContext.getContentResolver().update(contentUri, updateValues, null, null); 354 355 status = receiveFile(mFileInfo, op); 356 /* 357 * TODO map status to obex response code 358 */ 359 if (status != BluetoothShare.STATUS_SUCCESS) { 360 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 361 } 362 Constants.updateShareStatus(mContext, mInfo.mId, status); 363 } 364 365 if (status == BluetoothShare.STATUS_SUCCESS) { 366 Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE); 367 msg.obj = mInfo; 368 msg.sendToTarget(); 369 } else { 370 if (mCallback != null) { 371 Message msg = Message.obtain(mCallback, 372 BluetoothOppObexSession.MSG_SESSION_ERROR); 373 mInfo.mStatus = status; 374 msg.obj = mInfo; 375 msg.sendToTarget(); 376 } 377 } 378 } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED 379 || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) { 380 /* user actively deny the inbound transfer */ 381 /* 382 * Note There is a question: what's next if user deny the first obj? 383 * Option 1 :continue prompt for next objects 384 * Option 2 :reject next objects and finish the session 385 * Now we take option 2: 386 */ 387 388 Log.i(TAG, "Rejected incoming request"); 389 if (mFileInfo.mFileName != null) { 390 try { 391 mFileInfo.mOutputStream.close(); 392 } catch (IOException e) { 393 Log.e(TAG, "error close file stream"); 394 } 395 new File(mFileInfo.mFileName).delete(); 396 } 397 // set status as local cancel 398 status = BluetoothShare.STATUS_CANCELED; 399 Constants.updateShareStatus(mContext, mInfo.mId, status); 400 obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN; 401 402 Message msg = Message.obtain(mCallback); 403 msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED; 404 mInfo.mStatus = status; 405 msg.obj = mInfo; 406 msg.sendToTarget(); 407 } 408 return obexResponse; 409 } 410 receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op)411 private int receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op) { 412 /* 413 * implement receive file 414 */ 415 int status = -1; 416 BufferedOutputStream bos = null; 417 418 InputStream is = null; 419 boolean error = false; 420 try { 421 is = op.openInputStream(); 422 } catch (IOException e1) { 423 Log.e(TAG, "Error when openInputStream"); 424 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 425 error = true; 426 } 427 428 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 429 430 if (!error) { 431 ContentValues updateValues = new ContentValues(); 432 updateValues.put(BluetoothShare._DATA, fileInfo.mFileName); 433 mContext.getContentResolver().update(contentUri, updateValues, null, null); 434 } 435 436 int position = 0; 437 if (!error) { 438 bos = new BufferedOutputStream(fileInfo.mOutputStream, 0x10000); 439 } 440 441 if (!error) { 442 int outputBufferSize = op.getMaxPacketSize(); 443 byte[] b = new byte[outputBufferSize]; 444 int readLength = 0; 445 long timestamp = 0; 446 try { 447 while ((!mInterrupted) && (position != fileInfo.mLength)) { 448 449 if (V) timestamp = System.currentTimeMillis(); 450 451 readLength = is.read(b); 452 453 if (readLength == -1) { 454 if (D) Log.d(TAG, "Receive file reached stream end at position" + position); 455 break; 456 } 457 458 bos.write(b, 0, readLength); 459 position += readLength; 460 461 if (V) { 462 Log.v(TAG, "Receive file position = " + position + " readLength " 463 + readLength + " bytes took " 464 + (System.currentTimeMillis() - timestamp) + " ms"); 465 } 466 467 ContentValues updateValues = new ContentValues(); 468 updateValues.put(BluetoothShare.CURRENT_BYTES, position); 469 mContext.getContentResolver().update(contentUri, updateValues, null, null); 470 } 471 } catch (IOException e1) { 472 Log.e(TAG, "Error when receiving file"); 473 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 474 error = true; 475 } 476 } 477 478 if (mInterrupted) { 479 if (D) Log.d(TAG, "receiving file interrupted by user."); 480 status = BluetoothShare.STATUS_CANCELED; 481 } else { 482 if (position == fileInfo.mLength) { 483 if (D) Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName); 484 status = BluetoothShare.STATUS_SUCCESS; 485 } else { 486 if (D) Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength); 487 if (status == -1) { 488 status = BluetoothShare.STATUS_UNKNOWN_ERROR; 489 } 490 } 491 } 492 493 if (bos != null) { 494 try { 495 bos.close(); 496 } catch (IOException e) { 497 Log.e(TAG, "Error when closing stream after send"); 498 } 499 } 500 return status; 501 } 502 processShareInfo()503 private BluetoothOppReceiveFileInfo processShareInfo() { 504 if (D) Log.d(TAG, "processShareInfo() " + mInfo.mId); 505 BluetoothOppReceiveFileInfo fileInfo = BluetoothOppReceiveFileInfo.generateFileInfo( 506 mContext, mInfo.mId); 507 if (V) { 508 Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:"); 509 Log.v(TAG, "filename :" + fileInfo.mFileName); 510 Log.v(TAG, "length :" + fileInfo.mLength); 511 Log.v(TAG, "status :" + fileInfo.mStatus); 512 } 513 return fileInfo; 514 } 515 516 @Override onConnect(HeaderSet request, HeaderSet reply)517 public int onConnect(HeaderSet request, HeaderSet reply) { 518 519 if (D) Log.d(TAG, "onConnect"); 520 if (V) Constants.logHeader(request); 521 try { 522 byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET); 523 if (V) Log.v(TAG, "onConnect(): uuid =" + Arrays.toString(uuid)); 524 if(uuid != null) { 525 reply.setHeader(HeaderSet.WHO, uuid); 526 } 527 } catch (IOException e) { 528 Log.e(TAG, e.toString()); 529 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 530 } 531 mTimestamp = System.currentTimeMillis(); 532 return ResponseCodes.OBEX_HTTP_OK; 533 } 534 535 @Override onDisconnect(HeaderSet req, HeaderSet resp)536 public void onDisconnect(HeaderSet req, HeaderSet resp) { 537 if (D) Log.d(TAG, "onDisconnect"); 538 resp.responseCode = ResponseCodes.OBEX_HTTP_OK; 539 } 540 releaseWakeLocks()541 private synchronized void releaseWakeLocks() { 542 if (mWakeLock.isHeld()) { 543 mWakeLock.release(); 544 } 545 if (mPartialWakeLock.isHeld()) { 546 mPartialWakeLock.release(); 547 } 548 } 549 550 @Override onClose()551 public void onClose() { 552 if (V) Log.v(TAG, "release WakeLock"); 553 releaseWakeLocks(); 554 555 /* onClose could happen even before start() where mCallback is set */ 556 if (mCallback != null) { 557 Message msg = Message.obtain(mCallback); 558 msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE; 559 msg.obj = mInfo; 560 msg.sendToTarget(); 561 } 562 } 563 } 564