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