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