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