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