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