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