BluetoothOppObexClientSession.java revision 2fbb1d97d08d5d72fe824e543c714e56cd7be236
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 javax.obex.ClientOperation; 36import javax.obex.ClientSession; 37import javax.obex.HeaderSet; 38import javax.obex.ObexTransport; 39import javax.obex.ResponseCodes; 40 41import android.app.NotificationManager; 42import android.content.ContentValues; 43import android.content.Context; 44import android.net.Uri; 45import android.os.Handler; 46import android.os.Message; 47import android.os.PowerManager; 48import android.os.PowerManager.WakeLock; 49import android.os.Process; 50import android.util.Log; 51 52import java.io.BufferedInputStream; 53import java.io.IOException; 54import java.io.InputStream; 55import java.io.OutputStream; 56import java.lang.IllegalArgumentException; 57import java.lang.Thread; 58 59/** 60 * This class runs as an OBEX client 61 */ 62public class BluetoothOppObexClientSession implements BluetoothOppObexSession { 63 64 private static final String TAG = "BtOppObexClient"; 65 private static final boolean D = Constants.DEBUG; 66 private static final boolean V = Constants.VERBOSE; 67 68 private ClientThread mThread; 69 70 private ObexTransport mTransport; 71 72 private Context mContext; 73 74 private volatile boolean mInterrupted; 75 76 private volatile boolean mWaitingForRemote; 77 78 private Handler mCallback; 79 80 public BluetoothOppObexClientSession(Context context, ObexTransport transport) { 81 if (transport == null) { 82 throw new NullPointerException("transport is null"); 83 } 84 mContext = context; 85 mTransport = transport; 86 } 87 88 @Override 89 public void start(Handler handler, int numShares) { 90 if (D) Log.d(TAG, "Start!"); 91 mCallback = handler; 92 mThread = new ClientThread(mContext, mTransport, numShares); 93 mThread.start(); 94 } 95 96 @Override 97 public void stop() { 98 if (D) Log.d(TAG, "Stop!"); 99 if (mThread != null) { 100 mInterrupted = true; 101 try { 102 mThread.interrupt(); 103 if (V) Log.v(TAG, "waiting for thread to terminate"); 104 mThread.join(); 105 mThread = null; 106 } catch (InterruptedException e) { 107 if (V) Log.v(TAG, "Interrupted waiting for thread to join"); 108 } 109 } 110 NotificationManager nm = 111 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 112 nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS); 113 114 mCallback = null; 115 } 116 117 @Override 118 public void addShare(BluetoothOppShareInfo share) { 119 mThread.addShare(share); 120 } 121 122 private static int readFully(InputStream is, byte[] buffer, int size) throws IOException { 123 int done = 0; 124 while (done < size) { 125 int got = is.read(buffer, done, size - done); 126 if (got <= 0) break; 127 done += got; 128 } 129 return done; 130 } 131 132 private class ClientThread extends Thread { 133 134 private static final int sSleepTime = 500; 135 136 private Context mContext1; 137 138 private BluetoothOppShareInfo mInfo; 139 140 private volatile boolean waitingForShare; 141 142 private ObexTransport mTransport1; 143 144 private ClientSession mCs; 145 146 private WakeLock wakeLock; 147 148 private BluetoothOppSendFileInfo mFileInfo = null; 149 150 private boolean mConnected = false; 151 152 private int mNumShares; 153 154 public ClientThread(Context context, ObexTransport transport, int initialNumShares) { 155 super("BtOpp ClientThread"); 156 mContext1 = context; 157 mTransport1 = transport; 158 waitingForShare = true; 159 mWaitingForRemote = false; 160 mNumShares = initialNumShares; 161 PowerManager pm = (PowerManager)mContext1.getSystemService(Context.POWER_SERVICE); 162 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 163 } 164 165 public void addShare(BluetoothOppShareInfo info) { 166 mInfo = info; 167 mFileInfo = processShareInfo(); 168 waitingForShare = false; 169 } 170 171 @Override 172 public void run() { 173 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 174 175 if (V) Log.v(TAG, "acquire partial WakeLock"); 176 wakeLock.acquire(); 177 178 try { 179 Thread.sleep(100); 180 } catch (InterruptedException e1) { 181 if (V) Log.v(TAG, "Client thread was interrupted (1), exiting"); 182 mInterrupted = true; 183 } 184 if (!mInterrupted) { 185 connect(mNumShares); 186 } 187 188 while (!mInterrupted) { 189 if (!waitingForShare) { 190 doSend(); 191 } else { 192 try { 193 if (D) Log.d(TAG, "Client thread waiting for next share, sleep for " 194 + sSleepTime); 195 Thread.sleep(sSleepTime); 196 } catch (InterruptedException e) { 197 198 } 199 } 200 } 201 disconnect(); 202 203 if (wakeLock.isHeld()) { 204 if (V) Log.v(TAG, "release partial WakeLock"); 205 wakeLock.release(); 206 } 207 Message msg = Message.obtain(mCallback); 208 msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE; 209 msg.obj = mInfo; 210 msg.sendToTarget(); 211 212 } 213 214 private void disconnect() { 215 try { 216 if (mCs != null) { 217 mCs.disconnect(null); 218 } 219 mCs = null; 220 if (D) Log.d(TAG, "OBEX session disconnected"); 221 } catch (IOException e) { 222 Log.w(TAG, "OBEX session disconnect error" + e); 223 } 224 try { 225 if (mCs != null) { 226 if (D) Log.d(TAG, "OBEX session close mCs"); 227 mCs.close(); 228 if (D) Log.d(TAG, "OBEX session closed"); 229 } 230 } catch (IOException e) { 231 Log.w(TAG, "OBEX session close error" + e); 232 } 233 if (mTransport1 != null) { 234 try { 235 mTransport1.close(); 236 } catch (IOException e) { 237 Log.e(TAG, "mTransport.close error"); 238 } 239 240 } 241 } 242 243 private void connect(int numShares) { 244 if (D) Log.d(TAG, "Create ClientSession with transport " + mTransport1.toString()); 245 try { 246 mCs = new ClientSession(mTransport1); 247 mConnected = true; 248 } catch (IOException e1) { 249 Log.e(TAG, "OBEX session create error"); 250 } 251 if (mConnected) { 252 mConnected = false; 253 HeaderSet hs = new HeaderSet(); 254 hs.setHeader(HeaderSet.COUNT, (long) numShares); 255 synchronized (this) { 256 mWaitingForRemote = true; 257 } 258 try { 259 mCs.connect(hs); 260 if (D) Log.d(TAG, "OBEX session created"); 261 mConnected = true; 262 } catch (IOException e) { 263 Log.e(TAG, "OBEX session connect error"); 264 } 265 } 266 synchronized (this) { 267 mWaitingForRemote = false; 268 } 269 } 270 271 private void doSend() { 272 273 int status = BluetoothShare.STATUS_SUCCESS; 274 275 /* connection is established too fast to get first mInfo */ 276 while (mFileInfo == null) { 277 try { 278 Thread.sleep(50); 279 } catch (InterruptedException e) { 280 status = BluetoothShare.STATUS_CANCELED; 281 } 282 } 283 if (!mConnected) { 284 // Obex connection error 285 status = BluetoothShare.STATUS_CONNECTION_ERROR; 286 } 287 if (status == BluetoothShare.STATUS_SUCCESS) { 288 /* do real send */ 289 if (mFileInfo.mFileName != null) { 290 status = sendFile(mFileInfo); 291 } else { 292 /* this is invalid request */ 293 status = mFileInfo.mStatus; 294 } 295 waitingForShare = true; 296 } else { 297 Constants.updateShareStatus(mContext1, mInfo.mId, status); 298 } 299 300 Message msg = Message.obtain(mCallback); 301 msg.what = (status == BluetoothShare.STATUS_SUCCESS) ? 302 BluetoothOppObexSession.MSG_SHARE_COMPLETE : 303 BluetoothOppObexSession.MSG_SESSION_ERROR; 304 mInfo.mStatus = status; 305 msg.obj = mInfo; 306 msg.sendToTarget(); 307 } 308 309 /* 310 * Validate this ShareInfo 311 */ 312 private BluetoothOppSendFileInfo processShareInfo() { 313 if (V) Log.v(TAG, "Client thread processShareInfo() " + mInfo.mId); 314 315 BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(mInfo.mUri); 316 if (fileInfo.mFileName == null || fileInfo.mLength == 0) { 317 if (V) Log.v(TAG, "BluetoothOppSendFileInfo get invalid file"); 318 Constants.updateShareStatus(mContext1, mInfo.mId, fileInfo.mStatus); 319 320 } else { 321 if (V) { 322 Log.v(TAG, "Generate BluetoothOppSendFileInfo:"); 323 Log.v(TAG, "filename :" + fileInfo.mFileName); 324 Log.v(TAG, "length :" + fileInfo.mLength); 325 Log.v(TAG, "mimetype :" + fileInfo.mMimetype); 326 } 327 328 ContentValues updateValues = new ContentValues(); 329 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 330 331 updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName); 332 updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength); 333 updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype); 334 335 mContext1.getContentResolver().update(contentUri, updateValues, null, null); 336 337 } 338 return fileInfo; 339 } 340 341 private int sendFile(BluetoothOppSendFileInfo fileInfo) { 342 boolean error = false; 343 int responseCode = -1; 344 long position = 0; 345 int status = BluetoothShare.STATUS_SUCCESS; 346 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId); 347 ContentValues updateValues; 348 HeaderSet request = new HeaderSet(); 349 ClientOperation putOperation = null; 350 OutputStream outputStream = null; 351 InputStream inputStream = null; 352 try { 353 synchronized (this) { 354 mWaitingForRemote = true; 355 } 356 try { 357 if (V) Log.v(TAG, "Set header items for " + fileInfo.mFileName); 358 request.setHeader(HeaderSet.NAME, fileInfo.mFileName); 359 request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype); 360 361 applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName); 362 Constants.updateShareStatus( 363 mContext1, mInfo.mId, BluetoothShare.STATUS_RUNNING); 364 365 request.setHeader(HeaderSet.LENGTH, fileInfo.mLength); 366 367 if (V) Log.v(TAG, "put headerset for " + fileInfo.mFileName); 368 putOperation = (ClientOperation)mCs.put(request); 369 } catch (IllegalArgumentException e) { 370 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 371 Constants.updateShareStatus(mContext1, mInfo.mId, status); 372 373 Log.e(TAG, "Error setting header items for request: " + e); 374 error = true; 375 } catch (IOException e) { 376 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 377 Constants.updateShareStatus(mContext1, mInfo.mId, status); 378 379 Log.e(TAG, "Error when put HeaderSet "); 380 error = true; 381 } 382 synchronized (this) { 383 mWaitingForRemote = false; 384 } 385 386 if (!error) { 387 try { 388 if (V) Log.v(TAG, "openOutputStream " + fileInfo.mFileName); 389 outputStream = putOperation.openOutputStream(); 390 inputStream = putOperation.openInputStream(); 391 } catch (IOException e) { 392 status = BluetoothShare.STATUS_OBEX_DATA_ERROR; 393 Constants.updateShareStatus(mContext1, mInfo.mId, status); 394 Log.e(TAG, "Error when openOutputStream"); 395 error = true; 396 } 397 } 398 if (!error) { 399 updateValues = new ContentValues(); 400 updateValues.put(BluetoothShare.CURRENT_BYTES, 0); 401 updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING); 402 mContext1.getContentResolver().update(contentUri, updateValues, null, null); 403 } 404 405 if (!error) { 406 int readLength = 0; 407 long percent = 0; 408 long prevPercent = 0; 409 boolean okToProceed = false; 410 long timestamp = 0; 411 int outputBufferSize = putOperation.getMaxPacketSize(); 412 byte[] buffer = new byte[outputBufferSize]; 413 BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000); 414 415 if (!mInterrupted && (position != fileInfo.mLength)) { 416 readLength = readFully(a, buffer, outputBufferSize); 417 418 mCallback.sendMessageDelayed(mCallback 419 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT), 420 BluetoothOppObexSession.SESSION_TIMEOUT); 421 synchronized (this) { 422 mWaitingForRemote = true; 423 } 424 425 // first packet will block here 426 outputStream.write(buffer, 0, readLength); 427 428 position += readLength; 429 430 if (position == fileInfo.mLength) { 431 // if file length is smaller than buffer size, only one packet 432 // so block point is here 433 outputStream.close(); 434 outputStream = null; 435 } 436 437 /* check remote accept or reject */ 438 responseCode = putOperation.getResponseCode(); 439 440 mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT); 441 synchronized (this) { 442 mWaitingForRemote = false; 443 } 444 445 if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE 446 || responseCode == ResponseCodes.OBEX_HTTP_OK) { 447 if (V) Log.v(TAG, "Remote accept"); 448 okToProceed = true; 449 updateValues = new ContentValues(); 450 updateValues.put(BluetoothShare.CURRENT_BYTES, position); 451 mContext1.getContentResolver().update(contentUri, updateValues, null, 452 null); 453 } else { 454 Log.i(TAG, "Remote reject, Response code is " + responseCode); 455 } 456 } 457 458 while (!mInterrupted && okToProceed && (position < fileInfo.mLength)) { 459 if (V) timestamp = System.currentTimeMillis(); 460 461 readLength = a.read(buffer, 0, outputBufferSize); 462 outputStream.write(buffer, 0, readLength); 463 464 /* check remote abort */ 465 responseCode = putOperation.getResponseCode(); 466 if (V) Log.v(TAG, "Response code is " + responseCode); 467 if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE 468 && responseCode != ResponseCodes.OBEX_HTTP_OK) { 469 /* abort happens */ 470 okToProceed = false; 471 } else { 472 position += readLength; 473 if (V) { 474 Log.v(TAG, "Sending file position = " + position 475 + " readLength " + readLength + " bytes took " 476 + (System.currentTimeMillis() - timestamp) + " ms"); 477 } 478 // Update the Progress Bar only if there is change in percentage 479 percent = position * 100 / fileInfo.mLength; 480 if (percent > prevPercent) { 481 updateValues = new ContentValues(); 482 updateValues.put(BluetoothShare.CURRENT_BYTES, position); 483 mContext1.getContentResolver().update(contentUri, updateValues, 484 null, null); 485 prevPercent = percent; 486 } 487 } 488 } 489 490 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN 491 || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) { 492 Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length " 493 + fileInfo.mLength); 494 status = BluetoothShare.STATUS_FORBIDDEN; 495 } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) { 496 Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype); 497 status = BluetoothShare.STATUS_NOT_ACCEPTABLE; 498 } else if (!mInterrupted && position == fileInfo.mLength) { 499 Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName 500 + " length " + fileInfo.mLength); 501 } else { 502 error = true; 503 status = BluetoothShare.STATUS_CANCELED; 504 putOperation.abort(); 505 /* interrupted */ 506 Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName 507 + " at " + position + " of " + fileInfo.mLength); 508 } 509 } 510 } catch (IOException e) { 511 handleSendException(e.toString()); 512 } catch (NullPointerException e) { 513 handleSendException(e.toString()); 514 } catch (IndexOutOfBoundsException e) { 515 handleSendException(e.toString()); 516 } finally { 517 try { 518 if (outputStream != null) { 519 outputStream.close(); 520 } 521 522 // Close InputStream and remove SendFileInfo from map 523 BluetoothOppUtility.closeSendFileInfo(mInfo.mUri); 524 if (!error) { 525 responseCode = putOperation.getResponseCode(); 526 if (responseCode != -1) { 527 if (V) Log.v(TAG, "Get response code " + responseCode); 528 if (responseCode != ResponseCodes.OBEX_HTTP_OK) { 529 Log.i(TAG, "Response error code is " + responseCode); 530 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE; 531 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) { 532 status = BluetoothShare.STATUS_NOT_ACCEPTABLE; 533 } 534 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN 535 || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) { 536 status = BluetoothShare.STATUS_FORBIDDEN; 537 } 538 } 539 } else { 540 // responseCode is -1, which means connection error 541 status = BluetoothShare.STATUS_CONNECTION_ERROR; 542 } 543 } 544 545 Constants.updateShareStatus(mContext1, mInfo.mId, status); 546 547 if (inputStream != null) { 548 inputStream.close(); 549 } 550 if (putOperation != null) { 551 putOperation.close(); 552 } 553 } catch (IOException e) { 554 Log.e(TAG, "Error when closing stream after send"); 555 556 // Socket has been closed due to the response timeout in the framework, 557 // mark the transfer as failure. 558 if (position != fileInfo.mLength) { 559 status = BluetoothShare.STATUS_FORBIDDEN; 560 Constants.updateShareStatus(mContext1, mInfo.mId, status); 561 } 562 } 563 } 564 return status; 565 } 566 567 private void handleSendException(String exception) { 568 Log.e(TAG, "Error when sending file: " + exception); 569 // Update interrupted outbound content resolver entry when 570 // error during transfer. 571 Constants.updateShareStatus(mContext1, mInfo.mId, 572 BluetoothShare.STATUS_OBEX_DATA_ERROR); 573 mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT); 574 } 575 576 @Override 577 public void interrupt() { 578 super.interrupt(); 579 synchronized (this) { 580 if (mWaitingForRemote) { 581 if (V) Log.v(TAG, "Interrupted when waitingForRemote"); 582 try { 583 mTransport1.close(); 584 } catch (IOException e) { 585 Log.e(TAG, "mTransport.close error"); 586 } 587 Message msg = Message.obtain(mCallback); 588 msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED; 589 if (mInfo != null) { 590 msg.obj = mInfo; 591 } 592 msg.sendToTarget(); 593 } 594 } 595 } 596 } 597 598 public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) { 599 if (address == null) { 600 return; 601 } 602 if (address.startsWith("00:04:48")) { 603 // Poloroid Pogo 604 // Rejects filenames with more than one '.'. Rename to '_'. 605 // for example: 'a.b.jpg' -> 'a_b.jpg' 606 // 'abc.jpg' NOT CHANGED 607 char[] c = filename.toCharArray(); 608 boolean firstDot = true; 609 boolean modified = false; 610 for (int i = c.length - 1; i >= 0; i--) { 611 if (c[i] == '.') { 612 if (!firstDot) { 613 modified = true; 614 c[i] = '_'; 615 } 616 firstDot = false; 617 } 618 } 619 620 if (modified) { 621 String newFilename = new String(c); 622 request.setHeader(HeaderSet.NAME, newFilename); 623 Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename + 624 "\" to workaround Poloroid filename quirk"); 625 } 626 } 627 } 628 629 @Override 630 public void unblock() { 631 // Not used for client case 632 } 633 634} 635