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