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.FileNotFoundException;
60import java.io.IOException;
61import java.io.InputStream;
62import java.util.ArrayList;
63
64/**
65 * Performs the background Bluetooth OPP transfer. It also starts thread to
66 * accept incoming OPP connection.
67 */
68
69public 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 confirmUpdated = 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            confirmUpdated = 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 (confirmUpdated) {
740            if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmation updated");
741            /* Inbounds transfer user confirmation status changed, update the session server */
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.confirmStatusChanged();
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.confirmStatusChanged();
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