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