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