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