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