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