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