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