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