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