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