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