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