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