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