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