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