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