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