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 com.google.android.collect.Lists; 36import javax.obex.ObexTransport; 37 38import android.app.Service; 39import android.bluetooth.BluetoothAdapter; 40import android.content.BroadcastReceiver; 41import android.content.ContentResolver; 42import android.content.ContentValues; 43import android.content.Context; 44import android.content.Intent; 45import android.content.IntentFilter; 46import android.database.CharArrayBuffer; 47import android.database.ContentObserver; 48import android.database.Cursor; 49import android.media.MediaScannerConnection; 50import android.media.MediaScannerConnection.MediaScannerConnectionClient; 51import android.net.Uri; 52import android.os.Handler; 53import android.os.IBinder; 54import android.os.Message; 55import android.os.PowerManager; 56import android.util.Log; 57import android.os.Process; 58 59import java.io.FileNotFoundException; 60import java.io.IOException; 61import java.io.InputStream; 62import java.util.ArrayList; 63 64/** 65 * Performs the background Bluetooth OPP transfer. It also starts thread to 66 * accept incoming OPP connection. 67 */ 68 69public class BluetoothOppService extends Service { 70 private static final boolean D = Constants.DEBUG; 71 private static final boolean V = Constants.VERBOSE; 72 73 private boolean userAccepted = false; 74 75 private class BluetoothShareContentObserver extends ContentObserver { 76 77 public BluetoothShareContentObserver() { 78 super(new Handler()); 79 } 80 81 @Override 82 public void onChange(boolean selfChange) { 83 if (V) Log.v(TAG, "ContentObserver received notification"); 84 updateFromProvider(); 85 } 86 } 87 88 private static final String TAG = "BtOppService"; 89 90 /** Observer to get notified when the content observer's data changes */ 91 private BluetoothShareContentObserver mObserver; 92 93 /** Class to handle Notification Manager updates */ 94 private BluetoothOppNotification mNotifier; 95 96 private boolean mPendingUpdate; 97 98 private UpdateThread mUpdateThread; 99 100 private ArrayList<BluetoothOppShareInfo> mShares; 101 102 private ArrayList<BluetoothOppBatch> mBatchs; 103 104 private BluetoothOppTransfer mTransfer; 105 106 private BluetoothOppTransfer mServerTransfer; 107 108 private int mBatchId; 109 110 /** 111 * Array used when extracting strings from content provider 112 */ 113 private CharArrayBuffer mOldChars; 114 115 /** 116 * Array used when extracting strings from content provider 117 */ 118 private CharArrayBuffer mNewChars; 119 120 private BluetoothAdapter mAdapter; 121 122 private PowerManager mPowerManager; 123 124 private BluetoothOppRfcommListener mSocketListener; 125 126 private boolean mListenStarted = false; 127 128 private boolean mMediaScanInProgress; 129 130 private int mIncomingRetries = 0; 131 132 private ObexTransport mPendingConnection = null; 133 134 /* 135 * TODO No support for queue incoming from multiple devices. 136 * Make an array list of server session to support receiving queue from 137 * multiple devices 138 */ 139 private BluetoothOppObexServerSession mServerSession; 140 141 @Override 142 public IBinder onBind(Intent arg0) { 143 throw new UnsupportedOperationException("Cannot bind to Bluetooth OPP Service"); 144 } 145 146 @Override 147 public void onCreate() { 148 super.onCreate(); 149 if (V) Log.v(TAG, "onCreate"); 150 mAdapter = BluetoothAdapter.getDefaultAdapter(); 151 mSocketListener = new BluetoothOppRfcommListener(mAdapter); 152 mShares = Lists.newArrayList(); 153 mBatchs = Lists.newArrayList(); 154 mObserver = new BluetoothShareContentObserver(); 155 getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver); 156 mBatchId = 1; 157 mNotifier = new BluetoothOppNotification(this); 158 mNotifier.mNotificationMgr.cancelAll(); 159 mNotifier.updateNotification(); 160 161 final ContentResolver contentResolver = getContentResolver(); 162 new Thread("trimDatabase") { 163 public void run() { 164 trimDatabase(contentResolver); 165 } 166 }.start(); 167 168 IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 169 registerReceiver(mBluetoothReceiver, filter); 170 171 synchronized (BluetoothOppService.this) { 172 if (mAdapter == null) { 173 Log.w(TAG, "Local BT device is not enabled"); 174 } else { 175 startListener(); 176 } 177 } 178 if (V) BluetoothOppPreference.getInstance(this).dump(); 179 updateFromProvider(); 180 } 181 182 @Override 183 public int onStartCommand(Intent intent, int flags, int startId) { 184 if (V) Log.v(TAG, "onStartCommand"); 185 //int retCode = super.onStartCommand(intent, flags, startId); 186 //if (retCode == START_STICKY) { 187 if (mAdapter == null) { 188 Log.w(TAG, "Local BT device is not enabled"); 189 } else { 190 startListener(); 191 } 192 updateFromProvider(); 193 //} 194 return START_NOT_STICKY; 195 } 196 197 private void startListener() { 198 if (!mListenStarted) { 199 if (mAdapter.isEnabled()) { 200 if (V) Log.v(TAG, "Starting RfcommListener"); 201 mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER)); 202 mListenStarted = true; 203 } 204 } 205 } 206 207 private static final int START_LISTENER = 1; 208 209 private static final int MEDIA_SCANNED = 2; 210 211 private static final int MEDIA_SCANNED_FAILED = 3; 212 213 private static final int MSG_INCOMING_CONNECTION_RETRY = 4; 214 215 private static final int STOP_LISTENER = 200; 216 217 private Handler mHandler = new Handler() { 218 @Override 219 public void handleMessage(Message msg) { 220 switch (msg.what) { 221 case STOP_LISTENER: 222 if(mSocketListener != null){ 223 mSocketListener.stop(); 224 } 225 mListenStarted = false; 226 //Stop Active INBOUND Transfer 227 if(mServerTransfer != null){ 228 mServerTransfer.onBatchCanceled(); 229 mServerTransfer =null; 230 } 231 //Stop Active OUTBOUND Transfer 232 if(mTransfer != null){ 233 mTransfer.onBatchCanceled(); 234 mTransfer =null; 235 } 236 synchronized (BluetoothOppService.this) { 237 if (mUpdateThread == null) { 238 stopSelf(); 239 } 240 } 241 break; 242 case START_LISTENER: 243 if (mAdapter.isEnabled()) { 244 startSocketListener(); 245 } 246 break; 247 case MEDIA_SCANNED: 248 if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= " 249 + msg.obj.toString()); 250 ContentValues updateValues = new ContentValues(); 251 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1); 252 updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK); 253 updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update 254 updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType( 255 Uri.parse(msg.obj.toString()))); 256 getContentResolver().update(contentUri, updateValues, null, null); 257 synchronized (BluetoothOppService.this) { 258 mMediaScanInProgress = false; 259 } 260 break; 261 case MEDIA_SCANNED_FAILED: 262 Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED"); 263 ContentValues updateValues1 = new ContentValues(); 264 Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1); 265 updateValues1.put(Constants.MEDIA_SCANNED, 266 Constants.MEDIA_SCANNED_SCANNED_FAILED); 267 getContentResolver().update(contentUri1, updateValues1, null, null); 268 synchronized (BluetoothOppService.this) { 269 mMediaScanInProgress = false; 270 } 271 break; 272 case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION: 273 if (D) Log.d(TAG, "Get incoming connection"); 274 ObexTransport transport = (ObexTransport)msg.obj; 275 /* 276 * Strategy for incoming connections: 277 * 1. If there is no ongoing transfer, no on-hold connection, start it 278 * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times) 279 * 3. If there is on-hold connection, reject directly 280 */ 281 if (mBatchs.size() == 0 && mPendingConnection == null) { 282 Log.i(TAG, "Start Obex Server"); 283 createServerSession(transport); 284 } else { 285 if (mPendingConnection != null) { 286 Log.w(TAG, "OPP busy! Reject connection"); 287 try { 288 transport.close(); 289 } catch (IOException e) { 290 Log.e(TAG, "close tranport error"); 291 } 292 } else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) { 293 Log.i(TAG, "Start Obex Server in TCP DEBUG mode"); 294 createServerSession(transport); 295 } else { 296 Log.i(TAG, "OPP busy! Retry after 1 second"); 297 mIncomingRetries = mIncomingRetries + 1; 298 mPendingConnection = transport; 299 Message msg1 = Message.obtain(mHandler); 300 msg1.what = MSG_INCOMING_CONNECTION_RETRY; 301 mHandler.sendMessageDelayed(msg1, 1000); 302 } 303 } 304 break; 305 case MSG_INCOMING_CONNECTION_RETRY: 306 if (mBatchs.size() == 0) { 307 Log.i(TAG, "Start Obex Server"); 308 createServerSession(mPendingConnection); 309 mIncomingRetries = 0; 310 mPendingConnection = null; 311 } else { 312 if (mIncomingRetries == 20) { 313 Log.w(TAG, "Retried 20 seconds, reject connection"); 314 try { 315 mPendingConnection.close(); 316 } catch (IOException e) { 317 Log.e(TAG, "close tranport error"); 318 } 319 mIncomingRetries = 0; 320 mPendingConnection = null; 321 } else { 322 Log.i(TAG, "OPP busy! Retry after 1 second"); 323 mIncomingRetries = mIncomingRetries + 1; 324 Message msg2 = Message.obtain(mHandler); 325 msg2.what = MSG_INCOMING_CONNECTION_RETRY; 326 mHandler.sendMessageDelayed(msg2, 1000); 327 } 328 } 329 break; 330 } 331 } 332 }; 333 334 private void startSocketListener() { 335 336 if (V) Log.v(TAG, "start RfcommListener"); 337 mSocketListener.start(mHandler); 338 if (V) Log.v(TAG, "RfcommListener started"); 339 } 340 341 @Override 342 public void onDestroy() { 343 if (V) Log.v(TAG, "onDestroy"); 344 super.onDestroy(); 345 getContentResolver().unregisterContentObserver(mObserver); 346 unregisterReceiver(mBluetoothReceiver); 347 mSocketListener.stop(); 348 349 if(mBatchs != null) { 350 mBatchs.clear(); 351 } 352 if(mShares != null) { 353 mShares.clear(); 354 } 355 if(mHandler != null) { 356 mHandler.removeCallbacksAndMessages(null); 357 } 358 } 359 360 /* suppose we auto accept an incoming OPUSH connection */ 361 private void createServerSession(ObexTransport transport) { 362 mServerSession = new BluetoothOppObexServerSession(this, transport); 363 mServerSession.preStart(); 364 if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString() 365 + " for incoming connection" + transport.toString()); 366 } 367 368 private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() { 369 @Override 370 public void onReceive(Context context, Intent intent) { 371 String action = intent.getAction(); 372 373 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 374 switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { 375 case BluetoothAdapter.STATE_ON: 376 if (V) Log.v(TAG, 377 "Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON"); 378 startSocketListener(); 379 break; 380 case BluetoothAdapter.STATE_TURNING_OFF: 381 if (V) Log.v(TAG, "Receiver DISABLED_ACTION "); 382 //FIX: Don't block main thread 383 /* 384 mSocketListener.stop(); 385 mListenStarted = false; 386 synchronized (BluetoothOppService.this) { 387 if (mUpdateThread == null) { 388 stopSelf(); 389 } 390 } 391 */ 392 mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER)); 393 394 break; 395 } 396 } 397 } 398 }; 399 400 private void updateFromProvider() { 401 synchronized (BluetoothOppService.this) { 402 mPendingUpdate = true; 403 if (mUpdateThread == null) { 404 mUpdateThread = new UpdateThread(); 405 mUpdateThread.start(); 406 } 407 } 408 } 409 410 private class UpdateThread extends Thread { 411 public UpdateThread() { 412 super("Bluetooth Share Service"); 413 } 414 415 @Override 416 public void run() { 417 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 418 419 boolean keepService = false; 420 for (;;) { 421 synchronized (BluetoothOppService.this) { 422 if (mUpdateThread != this) { 423 throw new IllegalStateException( 424 "multiple UpdateThreads in BluetoothOppService"); 425 } 426 if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is " 427 + keepService + " sListenStarted is " + mListenStarted); 428 if (!mPendingUpdate) { 429 mUpdateThread = null; 430 if (!keepService && !mListenStarted) { 431 stopSelf(); 432 break; 433 } 434 return; 435 } 436 mPendingUpdate = false; 437 } 438 Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null, 439 null, BluetoothShare._ID); 440 441 if (cursor == null) { 442 return; 443 } 444 445 cursor.moveToFirst(); 446 447 int arrayPos = 0; 448 449 keepService = false; 450 boolean isAfterLast = cursor.isAfterLast(); 451 452 int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID); 453 /* 454 * Walk the cursor and the local array to keep them in sync. The 455 * key to the algorithm is that the ids are unique and sorted 456 * both in the cursor and in the array, so that they can be 457 * processed in order in both sources at the same time: at each 458 * step, both sources point to the lowest id that hasn't been 459 * processed from that source, and the algorithm processes the 460 * lowest id from those two possibilities. At each step: -If the 461 * array contains an entry that's not in the cursor, remove the 462 * entry, move to next entry in the array. -If the array 463 * contains an entry that's in the cursor, nothing to do, move 464 * to next cursor row and next array entry. -If the cursor 465 * contains an entry that's not in the array, insert a new entry 466 * in the array, move to next cursor row and next array entry. 467 */ 468 while (!isAfterLast || arrayPos < mShares.size()) { 469 if (isAfterLast) { 470 // We're beyond the end of the cursor but there's still 471 // some 472 // stuff in the local array, which can only be junk 473 if (mShares.size() != 0) 474 if (V) Log.v(TAG, "Array update: trimming " + 475 mShares.get(arrayPos).mId + " @ " + arrayPos); 476 477 if (shouldScanFile(arrayPos)) { 478 scanFile(null, arrayPos); 479 } 480 deleteShare(arrayPos); // this advances in the array 481 } else { 482 int id = cursor.getInt(idColumn); 483 484 if (arrayPos == mShares.size()) { 485 insertShare(cursor, arrayPos); 486 if (V) Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos); 487 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) { 488 keepService = true; 489 } 490 if (visibleNotification(arrayPos)) { 491 keepService = true; 492 } 493 if (needAction(arrayPos)) { 494 keepService = true; 495 } 496 497 ++arrayPos; 498 cursor.moveToNext(); 499 isAfterLast = cursor.isAfterLast(); 500 } else { 501 int arrayId = 0; 502 if (mShares.size() != 0) 503 arrayId = mShares.get(arrayPos).mId; 504 505 if (arrayId < id) { 506 if (V) Log.v(TAG, "Array update: removing " + arrayId + " @ " 507 + arrayPos); 508 if (shouldScanFile(arrayPos)) { 509 scanFile(null, arrayPos); 510 } 511 deleteShare(arrayPos); 512 } else if (arrayId == id) { 513 // This cursor row already exists in the stored 514 // array 515 updateShare(cursor, arrayPos, userAccepted); 516 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) { 517 keepService = true; 518 } 519 if (visibleNotification(arrayPos)) { 520 keepService = true; 521 } 522 if (needAction(arrayPos)) { 523 keepService = true; 524 } 525 526 ++arrayPos; 527 cursor.moveToNext(); 528 isAfterLast = cursor.isAfterLast(); 529 } else { 530 // This cursor entry didn't exist in the stored 531 // array 532 if (V) Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos); 533 insertShare(cursor, arrayPos); 534 535 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) { 536 keepService = true; 537 } 538 if (visibleNotification(arrayPos)) { 539 keepService = true; 540 } 541 if (needAction(arrayPos)) { 542 keepService = true; 543 } 544 ++arrayPos; 545 cursor.moveToNext(); 546 isAfterLast = cursor.isAfterLast(); 547 } 548 } 549 } 550 } 551 552 mNotifier.updateNotification(); 553 554 cursor.close(); 555 } 556 } 557 558 } 559 560 private void insertShare(Cursor cursor, int arrayPos) { 561 String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI)); 562 Uri uri; 563 if (uriString != null) { 564 uri = Uri.parse(uriString); 565 Log.d(TAG, "insertShare parsed URI: " + uri); 566 } else { 567 uri = null; 568 Log.e(TAG, "insertShare found null URI at cursor!"); 569 } 570 BluetoothOppShareInfo info = new BluetoothOppShareInfo( 571 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)), 572 uri, 573 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)), 574 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)), 575 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)), 576 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)), 577 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)), 578 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)), 579 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)), 580 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)), 581 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)), 582 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)), 583 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)), 584 cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED); 585 586 if (V) { 587 Log.v(TAG, "Service adding new entry"); 588 Log.v(TAG, "ID : " + info.mId); 589 // Log.v(TAG, "URI : " + ((info.mUri != null) ? "yes" : "no")); 590 Log.v(TAG, "URI : " + info.mUri); 591 Log.v(TAG, "HINT : " + info.mHint); 592 Log.v(TAG, "FILENAME: " + info.mFilename); 593 Log.v(TAG, "MIMETYPE: " + info.mMimetype); 594 Log.v(TAG, "DIRECTION: " + info.mDirection); 595 Log.v(TAG, "DESTINAT: " + info.mDestination); 596 Log.v(TAG, "VISIBILI: " + info.mVisibility); 597 Log.v(TAG, "CONFIRM : " + info.mConfirm); 598 Log.v(TAG, "STATUS : " + info.mStatus); 599 Log.v(TAG, "TOTAL : " + info.mTotalBytes); 600 Log.v(TAG, "CURRENT : " + info.mCurrentBytes); 601 Log.v(TAG, "TIMESTAMP : " + info.mTimestamp); 602 Log.v(TAG, "SCANNED : " + info.mMediaScanned); 603 } 604 605 mShares.add(arrayPos, info); 606 607 /* Mark the info as failed if it's in invalid status */ 608 if (info.isObsolete()) { 609 Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR); 610 } 611 /* 612 * Add info into a batch. The logic is 613 * 1) Only add valid and readyToStart info 614 * 2) If there is no batch, create a batch and insert this transfer into batch, 615 * then run the batch 616 * 3) If there is existing batch and timestamp match, insert transfer into batch 617 * 4) If there is existing batch and timestamp does not match, create a new batch and 618 * put in queue 619 */ 620 621 if (info.isReadyToStart()) { 622 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { 623 /* check if the file exists */ 624 BluetoothOppSendFileInfo sendFileInfo = BluetoothOppUtility.getSendFileInfo( 625 info.mUri); 626 if (sendFileInfo == null || sendFileInfo.mInputStream == null) { 627 Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId); 628 Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST); 629 BluetoothOppUtility.closeSendFileInfo(info.mUri); 630 return; 631 } 632 } 633 if (mBatchs.size() == 0) { 634 BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info); 635 newBatch.mId = mBatchId; 636 mBatchId++; 637 mBatchs.add(newBatch); 638 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { 639 if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId 640 + " for OUTBOUND info " + info.mId); 641 mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch); 642 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) { 643 if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId 644 + " for INBOUND info " + info.mId); 645 mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch, 646 mServerSession); 647 } 648 649 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) { 650 if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId 651 + " for info " + info.mId); 652 mTransfer.start(); 653 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND 654 && mServerTransfer != null) { 655 if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId 656 + " for info " + info.mId); 657 mServerTransfer.start(); 658 } 659 660 } else { 661 int i = findBatchWithTimeStamp(info.mTimestamp); 662 if (i != -1) { 663 if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch " 664 + mBatchs.get(i).mId); 665 mBatchs.get(i).addShare(info); 666 } else { 667 // There is ongoing batch 668 BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info); 669 newBatch.mId = mBatchId; 670 mBatchId++; 671 mBatchs.add(newBatch); 672 if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " + 673 info.mId); 674 if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) { 675 // only allow concurrent serverTransfer in debug mode 676 if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) { 677 if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " + 678 newBatch.mId + " for info " + info.mId); 679 mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, 680 newBatch, mServerSession); 681 mServerTransfer.start(); 682 } 683 } 684 } 685 } 686 } 687 } 688 689 private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) { 690 BluetoothOppShareInfo info = mShares.get(arrayPos); 691 int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 692 693 info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); 694 if (info.mUri != null) { 695 info.mUri = Uri.parse(stringFromCursor(info.mUri.toString(), cursor, 696 BluetoothShare.URI)); 697 } else { 698 Log.w(TAG, "updateShare() called for ID " + info.mId + " with null URI"); 699 } 700 info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT); 701 info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA); 702 info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE); 703 info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)); 704 info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION); 705 int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)); 706 707 boolean confirmUpdated = false; 708 int newConfirm = cursor.getInt(cursor 709 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)); 710 711 if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE 712 && newVisibility != BluetoothShare.VISIBILITY_VISIBLE 713 && (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) { 714 mNotifier.mNotificationMgr.cancel(info.mId); 715 } 716 717 info.mVisibility = newVisibility; 718 719 if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING 720 && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) { 721 confirmUpdated = true; 722 } 723 info.mConfirm = cursor.getInt(cursor 724 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)); 725 int newStatus = cursor.getInt(statusColumn); 726 727 if (!BluetoothShare.isStatusCompleted(info.mStatus) 728 && BluetoothShare.isStatusCompleted(newStatus)) { 729 mNotifier.mNotificationMgr.cancel(info.mId); 730 } 731 732 info.mStatus = newStatus; 733 info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)); 734 info.mCurrentBytes = cursor.getInt(cursor 735 .getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)); 736 info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); 737 info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED); 738 739 if (confirmUpdated) { 740 if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmation updated"); 741 /* Inbounds transfer user confirmation status changed, update the session server */ 742 int i = findBatchWithTimeStamp(info.mTimestamp); 743 if (i != -1) { 744 BluetoothOppBatch batch = mBatchs.get(i); 745 if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) { 746 mServerTransfer.confirmStatusChanged(); 747 } //TODO need to think about else 748 } 749 } 750 int i = findBatchWithTimeStamp(info.mTimestamp); 751 if (i != -1) { 752 BluetoothOppBatch batch = mBatchs.get(i); 753 if (batch.mStatus == Constants.BATCH_STATUS_FINISHED 754 || batch.mStatus == Constants.BATCH_STATUS_FAILED) { 755 if (V) Log.v(TAG, "Batch " + batch.mId + " is finished"); 756 if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { 757 if (mTransfer == null) { 758 Log.e(TAG, "Unexpected error! mTransfer is null"); 759 } else if (batch.mId == mTransfer.getBatchId()) { 760 mTransfer.stop(); 761 } else { 762 Log.e(TAG, "Unexpected error! batch id " + batch.mId 763 + " doesn't match mTransfer id " + mTransfer.getBatchId()); 764 } 765 mTransfer = null; 766 } else { 767 if (mServerTransfer == null) { 768 Log.e(TAG, "Unexpected error! mServerTransfer is null"); 769 } else if (batch.mId == mServerTransfer.getBatchId()) { 770 mServerTransfer.stop(); 771 } else { 772 Log.e(TAG, "Unexpected error! batch id " + batch.mId 773 + " doesn't match mServerTransfer id " 774 + mServerTransfer.getBatchId()); 775 } 776 mServerTransfer = null; 777 } 778 removeBatch(batch); 779 } 780 } 781 } 782 783 /** 784 * Removes the local copy of the info about a share. 785 */ 786 private void deleteShare(int arrayPos) { 787 BluetoothOppShareInfo info = mShares.get(arrayPos); 788 789 /* 790 * Delete arrayPos from a batch. The logic is 791 * 1) Search existing batch for the info 792 * 2) cancel the batch 793 * 3) If the batch become empty delete the batch 794 */ 795 int i = findBatchWithTimeStamp(info.mTimestamp); 796 if (i != -1) { 797 BluetoothOppBatch batch = mBatchs.get(i); 798 if (batch.hasShare(info)) { 799 if (V) Log.v(TAG, "Service cancel batch for share " + info.mId); 800 batch.cancelBatch(); 801 } 802 if (batch.isEmpty()) { 803 if (V) Log.v(TAG, "Service remove batch " + batch.mId); 804 removeBatch(batch); 805 } 806 } 807 mShares.remove(arrayPos); 808 } 809 810 private String stringFromCursor(String old, Cursor cursor, String column) { 811 int index = cursor.getColumnIndexOrThrow(column); 812 if (old == null) { 813 return cursor.getString(index); 814 } 815 if (mNewChars == null) { 816 mNewChars = new CharArrayBuffer(128); 817 } 818 cursor.copyStringToBuffer(index, mNewChars); 819 int length = mNewChars.sizeCopied; 820 if (length != old.length()) { 821 return cursor.getString(index); 822 } 823 if (mOldChars == null || mOldChars.sizeCopied < length) { 824 mOldChars = new CharArrayBuffer(length); 825 } 826 char[] oldArray = mOldChars.data; 827 char[] newArray = mNewChars.data; 828 old.getChars(0, length, oldArray, 0); 829 for (int i = length - 1; i >= 0; --i) { 830 if (oldArray[i] != newArray[i]) { 831 return new String(newArray, 0, length); 832 } 833 } 834 return old; 835 } 836 837 private int findBatchWithTimeStamp(long timestamp) { 838 for (int i = mBatchs.size() - 1; i >= 0; i--) { 839 if (mBatchs.get(i).mTimestamp == timestamp) { 840 return i; 841 } 842 } 843 return -1; 844 } 845 846 private void removeBatch(BluetoothOppBatch batch) { 847 if (V) Log.v(TAG, "Remove batch " + batch.mId); 848 mBatchs.remove(batch); 849 BluetoothOppBatch nextBatch; 850 if (mBatchs.size() > 0) { 851 for (int i = 0; i < mBatchs.size(); i++) { 852 // we have a running batch 853 nextBatch = mBatchs.get(i); 854 if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) { 855 return; 856 } else { 857 // just finish a transfer, start pending outbound transfer 858 if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { 859 if (V) Log.v(TAG, "Start pending outbound batch " + nextBatch.mId); 860 mTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch); 861 mTransfer.start(); 862 return; 863 } else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND 864 && mServerSession != null) { 865 // have to support pending inbound transfer 866 // if an outbound transfer and incoming socket happens together 867 if (V) Log.v(TAG, "Start pending inbound batch " + nextBatch.mId); 868 mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch, 869 mServerSession); 870 mServerTransfer.start(); 871 if (nextBatch.getPendingShare().mConfirm == 872 BluetoothShare.USER_CONFIRMATION_CONFIRMED) { 873 mServerTransfer.confirmStatusChanged(); 874 } 875 return; 876 } 877 } 878 } 879 } 880 } 881 882 private boolean needAction(int arrayPos) { 883 BluetoothOppShareInfo info = mShares.get(arrayPos); 884 if (BluetoothShare.isStatusCompleted(info.mStatus)) { 885 return false; 886 } 887 return true; 888 } 889 890 private boolean visibleNotification(int arrayPos) { 891 BluetoothOppShareInfo info = mShares.get(arrayPos); 892 return info.hasCompletionNotification(); 893 } 894 895 private boolean scanFile(Cursor cursor, int arrayPos) { 896 BluetoothOppShareInfo info = mShares.get(arrayPos); 897 synchronized (BluetoothOppService.this) { 898 if (D) Log.d(TAG, "Scanning file " + info.mFilename); 899 if (!mMediaScanInProgress) { 900 mMediaScanInProgress = true; 901 new MediaScannerNotifier(this, info, mHandler); 902 return true; 903 } else { 904 return false; 905 } 906 } 907 } 908 909 private boolean shouldScanFile(int arrayPos) { 910 BluetoothOppShareInfo info = mShares.get(arrayPos); 911 return BluetoothShare.isStatusSuccess(info.mStatus) 912 && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned && 913 info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED; 914 } 915 916 // Run in a background thread at boot. 917 private static void trimDatabase(ContentResolver contentResolver) { 918 final String INVISIBLE = BluetoothShare.VISIBILITY + "=" + 919 BluetoothShare.VISIBILITY_HIDDEN; 920 921 // remove the invisible/complete/outbound shares 922 final String WHERE_INVISIBLE_COMPLETE_OUTBOUND = BluetoothShare.DIRECTION + "=" 923 + BluetoothShare.DIRECTION_OUTBOUND + " AND " + BluetoothShare.STATUS + ">=" 924 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE; 925 int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI, 926 WHERE_INVISIBLE_COMPLETE_OUTBOUND, null); 927 if (V) Log.v(TAG, "Deleted complete outbound shares, number = " + delNum); 928 929 // remove the invisible/finished/inbound/failed shares 930 final String WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED = BluetoothShare.DIRECTION + "=" 931 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + ">" 932 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE; 933 delNum = contentResolver.delete(BluetoothShare.CONTENT_URI, 934 WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED, null); 935 if (V) Log.v(TAG, "Deleted complete inbound failed shares, number = " + delNum); 936 937 // Only keep the inbound and successful shares for LiverFolder use 938 // Keep the latest 1000 to easy db query 939 final String WHERE_INBOUND_SUCCESS = BluetoothShare.DIRECTION + "=" 940 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + "=" 941 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE; 942 Cursor cursor = contentResolver.query(BluetoothShare.CONTENT_URI, new String[] { 943 BluetoothShare._ID 944 }, WHERE_INBOUND_SUCCESS, null, BluetoothShare._ID); // sort by id 945 946 if (cursor == null) { 947 return; 948 } 949 950 int recordNum = cursor.getCount(); 951 if (recordNum > Constants.MAX_RECORDS_IN_DATABASE) { 952 int numToDelete = recordNum - Constants.MAX_RECORDS_IN_DATABASE; 953 954 if (cursor.moveToPosition(numToDelete)) { 955 int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID); 956 long id = cursor.getLong(columnId); 957 delNum = contentResolver.delete(BluetoothShare.CONTENT_URI, 958 BluetoothShare._ID + " < " + id, null); 959 if (V) Log.v(TAG, "Deleted old inbound success share: " + delNum); 960 } 961 } 962 cursor.close(); 963 } 964 965 private static class MediaScannerNotifier implements MediaScannerConnectionClient { 966 967 private MediaScannerConnection mConnection; 968 969 private BluetoothOppShareInfo mInfo; 970 971 private Context mContext; 972 973 private Handler mCallback; 974 975 public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) { 976 mContext = context; 977 mInfo = info; 978 mCallback = handler; 979 mConnection = new MediaScannerConnection(mContext, this); 980 if (V) Log.v(TAG, "Connecting to MediaScannerConnection "); 981 mConnection.connect(); 982 } 983 984 public void onMediaScannerConnected() { 985 if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected"); 986 mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype); 987 } 988 989 public void onScanCompleted(String path, Uri uri) { 990 try { 991 if (V) { 992 Log.v(TAG, "MediaScannerConnection onScanCompleted"); 993 Log.v(TAG, "MediaScannerConnection path is " + path); 994 Log.v(TAG, "MediaScannerConnection Uri is " + uri); 995 } 996 if (uri != null) { 997 Message msg = Message.obtain(); 998 msg.setTarget(mCallback); 999 msg.what = MEDIA_SCANNED; 1000 msg.arg1 = mInfo.mId; 1001 msg.obj = uri; 1002 msg.sendToTarget(); 1003 } else { 1004 Message msg = Message.obtain(); 1005 msg.setTarget(mCallback); 1006 msg.what = MEDIA_SCANNED_FAILED; 1007 msg.arg1 = mInfo.mId; 1008 msg.sendToTarget(); 1009 } 1010 } catch (Exception ex) { 1011 Log.v(TAG, "!!!MediaScannerConnection exception: " + ex); 1012 } finally { 1013 if (V) Log.v(TAG, "MediaScannerConnection disconnect"); 1014 mConnection.disconnect(); 1015 } 1016 } 1017 } 1018} 1019