BluetoothOppObexServerSession.java revision 32e47df203390052c1ef771d78b22fc4aa5f9e72
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; 39 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.Intent; 43import android.net.Uri; 44import android.os.Handler; 45import android.os.Message; 46import android.os.PowerManager; 47import android.os.PowerManager.WakeLock; 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 = "BtOppObexServer"; 65 private static final boolean D = Constants.DEBUG; 66 private static final boolean V = Constants.VERBOSE; 67 68 private ObexTransport mTransport; 69 70 private Context mContext; 71 72 private Handler mCallback = null; 73 74 /* status when server is blocking for user/auto confirmation */ 75 private boolean mServerBlocking = true; 76 77 /* the current transfer info */ 78 private BluetoothOppShareInfo mInfo; 79 80 /* info id when we insert the record */ 81 private int mLocalShareInfoId; 82 83 private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING; 84 85 private boolean mInterrupted = false; 86 87 private ServerSession mSession; 88 89 private long mTimestamp; 90 91 private BluetoothOppReceiveFileInfo mFileInfo; 92 93 private WakeLock mWakeLock; 94 95 private WakeLock mPartialWakeLock; 96 97 boolean mTimeoutMsgSent = false; 98 99 public BluetoothOppObexServerSession(Context context, ObexTransport transport) { 100 mContext = context; 101 mTransport = transport; 102 PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); 103 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP 104 | PowerManager.ON_AFTER_RELEASE, TAG); 105 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 106 } 107 108 public void unblock() { 109 mServerBlocking = false; 110 } 111 112 /** 113 * Called when connection is accepted from remote, to retrieve the first 114 * Header then wait for user confirmation 115 */ 116 public void preStart() { 117 if (D) Log.d(TAG, "acquire full WakeLock"); 118 mWakeLock.acquire(); 119 try { 120 if (D) Log.d(TAG, "Create ServerSession with transport " + mTransport.toString()); 121 mSession = new ServerSession(mTransport, this, null); 122 } catch (IOException e) { 123 Log.e(TAG, "Create server session error" + e); 124 } 125 } 126 127 /** 128 * Called from BluetoothOppTransfer to start the "Transfer" 129 */ 130 public void start(Handler handler) { 131 if (D) Log.d(TAG, "Start!"); 132 mCallback = handler; 133 134 } 135 136 /** 137 * Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise, 138 * server should end by itself. 139 */ 140 public void stop() { 141 /* 142 * TODO now we implement in a tough way, just close the socket. 143 * maybe need nice way 144 */ 145 if (D) Log.d(TAG, "Stop!"); 146 mInterrupted = true; 147 if (mSession != null) { 148 try { 149 mSession.close(); 150 mTransport.close(); 151 } catch (IOException e) { 152 Log.e(TAG, "close mTransport error" + e); 153 } 154 } 155 mCallback = null; 156 mSession = null; 157 } 158 159 public void addShare(BluetoothOppShareInfo info) { 160 if (D) Log.d(TAG, "addShare for id " + info.mId); 161 mInfo = info; 162 mFileInfo = processShareInfo(); 163 } 164 165 @Override 166 public int onPut(Operation op) { 167 if (D) Log.d(TAG, "onPut " + op.toString()); 168 HeaderSet request; 169 String name, mimeType; 170 Long length; 171 172 int obexResponse = ResponseCodes.OBEX_HTTP_OK; 173 174 /** 175 * For multiple objects, reject further objects after user deny the 176 * first one 177 */ 178 if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) { 179 return ResponseCodes.OBEX_HTTP_FORBIDDEN; 180 } 181 182 try { 183 boolean pre_reject = false; 184 request = op.getReceivedHeader(); 185 if (V) Constants.logHeader(request); 186 name = (String)request.getHeader(HeaderSet.NAME); 187 length = (Long)request.getHeader(HeaderSet.LENGTH); 188 mimeType = (String)request.getHeader(HeaderSet.TYPE); 189 190 if (length == 0) { 191 if (D) Log.w(TAG, "length is 0, reject the transfer"); 192 pre_reject = true; 193 obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED; 194 } 195 196 if (name == null || name.equals("")) { 197 if (D) Log.w(TAG, "name is null or empty, reject the transfer"); 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 (D) Log.w(TAG, "There is no file extension, reject the transfer"); 208 pre_reject = true; 209 obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST; 210 } else { 211 extension = name.substring(dotIndex + 1).toLowerCase(); 212 MimeTypeMap map = MimeTypeMap.getSingleton(); 213 type = map.getMimeTypeFromExtension(extension); 214 if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type); 215 if (type != null) { 216 mimeType = type; 217 218 } else { 219 if (mimeType == null) { 220 if (D) Log.w(TAG, "Can't get mimetype, reject the transfer"); 221 pre_reject = true; 222 obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 223 } 224 } 225 if (mimeType != null) { 226 mimeType = mimeType.toLowerCase(); 227 } 228 } 229 } 230 231 if (!pre_reject 232 && (mimeType == null || Constants.mimeTypeMatches(mimeType, 233 Constants.UNACCEPTABLE_SHARE_INBOUND_TYPES))) { 234 if (D) Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer"); 235 pre_reject = true; 236 obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE; 237 } 238 239 if (pre_reject && obexResponse != ResponseCodes.OBEX_HTTP_OK) { 240 // some bad implemented client won't send disconnect 241 return obexResponse; 242 } 243 244 } catch (IOException e) { 245 Log.e(TAG, "get getReceivedHeaders error " + e); 246 return ResponseCodes.OBEX_HTTP_BAD_REQUEST; 247 } 248 249 ContentValues values = new ContentValues(); 250 251 values.put(BluetoothShare.FILENAME_HINT, name); 252 values.put(BluetoothShare.TOTAL_BYTES, length.intValue()); 253 values.put(BluetoothShare.MIMETYPE, mimeType); 254 255 if (mTransport instanceof BluetoothOppRfcommTransport) { 256 String a = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress(); 257 values.put(BluetoothShare.DESTINATION, a); 258 } else { 259 values.put(BluetoothShare.DESTINATION, "FF:FF:FF:00:00:00"); 260 } 261 262 values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND); 263 values.put(BluetoothShare.TIMESTAMP, mTimestamp); 264 265 boolean needConfirm = true; 266 /** It's not first put if !serverBlocking, so we auto accept it */ 267 if (!mServerBlocking) { 268 values.put(BluetoothShare.USER_CONFIRMATION, 269 BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED); 270 needConfirm = false; 271 } 272 273 Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values); 274 mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1)); 275 276 if (needConfirm) { 277 Intent in = new Intent(BluetoothShare.INCOMING_FILE_CONFIRMATION_REQUEST_ACTION); 278 in.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 279 mContext.sendBroadcast(in); 280 } 281 282 if (V) Log.v(TAG, "insert contentUri: " + contentUri); 283 Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId); 284 285 if (V) Log.v(TAG, "acquire partial WakeLock"); 286 if (mWakeLock.isHeld()) { 287 mPartialWakeLock.acquire(); 288 mWakeLock.release(); 289 } 290 291 mServerBlocking = true; 292 293 synchronized (this) { 294 try { 295 296 while (mServerBlocking) { 297 wait(1000); 298 if (mCallback != null && !mTimeoutMsgSent) { 299 mCallback.sendMessageDelayed(mCallback 300 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT), 301 BluetoothOppObexSession.SESSION_TIMEOUT); 302 mTimeoutMsgSent = true; 303 if (V) Log.v(TAG, "MSG_CONNECT_TIMEOUT sent"); 304 } 305 } 306 } catch (InterruptedException e) { 307 if (V) Log.v(TAG, "Interrupted in onPut blocking"); 308 } 309 } 310 if (D) Log.d(TAG, "Server unblocked "); 311 if (mCallback != null && mTimeoutMsgSent) { 312 mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT); 313 } 314 315 /* we should have mInfo now */ 316 317 /* 318 * TODO check if this mInfo match the one that we insert before server 319 * blocking? just to make sure no error happens 320 */ 321 if (mInfo.mId != mLocalShareInfoId) { 322 Log.e(TAG, "Unexpected error!"); 323 } 324 mAccepted = mInfo.mConfirm; 325 326 if (V) Log.v(TAG, "after confirm: userAccepted=" + mAccepted); 327 int status = BluetoothShare.STATUS_SUCCESS; 328 329 if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED 330 || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED) { 331 /* Confirm or auto-confirm */ 332 333 if (mFileInfo.mFileName == null) { 334 status = mFileInfo.mStatus; 335 /* TODO need to check if this line is correct */ 336 mInfo.mStatus = mFileInfo.mStatus; 337 Constants.updateShareStatus(mContext, mInfo.mId, status); 338 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 339 340 } 341 342 if (mFileInfo.mFileName != null) { 343 344 ContentValues updateValues = new ContentValues(); 345 contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 346 updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName); 347 updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING); 348 mContext.getContentResolver().update(contentUri, updateValues, null, null); 349 350 status = receiveFile(mFileInfo, op); 351 /* 352 * TODO map status to obex response code 353 */ 354 if (status != BluetoothShare.STATUS_SUCCESS) { 355 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 356 } 357 Constants.updateShareStatus(mContext, mInfo.mId, status); 358 } 359 360 if (status == BluetoothShare.STATUS_SUCCESS) { 361 Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE); 362 msg.obj = mInfo; 363 msg.sendToTarget(); 364 } else { 365 if (mCallback != null) { 366 Message msg = Message.obtain(mCallback, 367 BluetoothOppObexSession.MSG_SESSION_ERROR); 368 mInfo.mStatus = status; 369 msg.obj = mInfo; 370 msg.sendToTarget(); 371 } 372 } 373 } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED 374 || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) { 375 /* user actively deny the inbound transfer */ 376 /* 377 * Note There is a question: what's next if user deny the first obj? 378 * Option 1 :continue prompt for next objects 379 * Option 2 :reject next objects and finish the session 380 * Now we take option 2: 381 */ 382 383 Log.i(TAG, "Rejected incoming request"); 384 if (mFileInfo.mFileName != null) { 385 try { 386 mFileInfo.mOutputStream.close(); 387 } catch (IOException e) { 388 Log.e(TAG, "error close file stream"); 389 } 390 new File(mFileInfo.mFileName).delete(); 391 } 392 // set status as local cancel 393 status = BluetoothShare.STATUS_CANCELED; 394 Constants.updateShareStatus(mContext, mInfo.mId, status); 395 obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN; 396 397 Message msg = Message.obtain(mCallback); 398 msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED; 399 mInfo.mStatus = status; 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 bos = new BufferedOutputStream(fileInfo.mOutputStream, 0x10000); 434 } 435 436 if (!error) { 437 int outputBufferSize = op.getMaxPacketSize(); 438 byte[] b = new byte[outputBufferSize]; 439 int readLength = 0; 440 long timestamp = 0; 441 try { 442 while ((!mInterrupted) && (position != fileInfo.mLength)) { 443 444 if (V) timestamp = System.currentTimeMillis(); 445 446 readLength = is.read(b); 447 448 if (readLength == -1) { 449 if (D) Log.d(TAG, "Receive file reached stream end at position" + position); 450 break; 451 } 452 453 bos.write(b, 0, readLength); 454 position += readLength; 455 456 if (V) { 457 Log.v(TAG, "Receive file position = " + position + " readLength " 458 + readLength + " bytes took " 459 + (System.currentTimeMillis() - timestamp) + " ms"); 460 } 461 462 ContentValues updateValues = new ContentValues(); 463 updateValues.put(BluetoothShare.CURRENT_BYTES, position); 464 mContext.getContentResolver().update(contentUri, updateValues, null, null); 465 } 466 } catch (IOException e1) { 467 Log.e(TAG, "Error when receiving file"); 468 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 469 error = true; 470 } 471 } 472 473 if (mInterrupted) { 474 if (D) Log.d(TAG, "receiving file interrupted by user."); 475 status = BluetoothShare.STATUS_CANCELED; 476 } else { 477 if (position == fileInfo.mLength) { 478 if (D) Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName); 479 status = BluetoothShare.STATUS_SUCCESS; 480 } else { 481 if (D) Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength); 482 if (status == -1) { 483 status = BluetoothShare.STATUS_UNKNOWN_ERROR; 484 } 485 } 486 } 487 488 if (bos != null) { 489 try { 490 bos.close(); 491 } catch (IOException e) { 492 Log.e(TAG, "Error when closing stream after send"); 493 } 494 } 495 return status; 496 } 497 498 private BluetoothOppReceiveFileInfo processShareInfo() { 499 if (D) Log.d(TAG, "processShareInfo() " + mInfo.mId); 500 BluetoothOppReceiveFileInfo fileInfo = BluetoothOppReceiveFileInfo.generateFileInfo( 501 mContext, mInfo.mId); 502 if (V) { 503 Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:"); 504 Log.v(TAG, "filename :" + fileInfo.mFileName); 505 Log.v(TAG, "length :" + fileInfo.mLength); 506 Log.v(TAG, "status :" + fileInfo.mStatus); 507 } 508 return fileInfo; 509 } 510 511 @Override 512 public int onConnect(HeaderSet request, HeaderSet reply) { 513 514 if (D) Log.d(TAG, "onConnect"); 515 if (V) Constants.logHeader(request); 516 517 mTimestamp = System.currentTimeMillis(); 518 return ResponseCodes.OBEX_HTTP_OK; 519 } 520 521 @Override 522 public void onDisconnect(HeaderSet req, HeaderSet resp) { 523 524 if (D) Log.d(TAG, "onDisconnect"); 525 resp.responseCode = ResponseCodes.OBEX_HTTP_OK; 526 527 /* onDisconnect could happen even before start() where mCallback is set */ 528 if (mCallback != null) { 529 Message msg = Message.obtain(mCallback); 530 msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE; 531 msg.obj = mInfo; 532 msg.sendToTarget(); 533 } 534 } 535 536 @Override 537 public void onClose() { 538 if (V) Log.v(TAG, "release WakeLock"); 539 if (mWakeLock.isHeld()) { 540 mWakeLock.release(); 541 } 542 if (mPartialWakeLock.isHeld()) { 543 mPartialWakeLock.release(); 544 } 545 } 546} 547