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