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