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