BluetoothOppService.java revision 888485a3f5fe991116c5536bb6d6903d47b63a70
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.bluetooth.BluetoothDevice;
41import android.bluetooth.BluetoothError;
42import android.bluetooth.BluetoothIntent;
43import android.content.BroadcastReceiver;
44import android.content.ContentUris;
45import android.content.ContentValues;
46import android.content.Context;
47import android.content.Intent;
48import android.content.IntentFilter;
49import android.database.CharArrayBuffer;
50import android.database.ContentObserver;
51import android.database.Cursor;
52import android.media.MediaScannerConnection;
53import android.media.MediaScannerConnection.MediaScannerConnectionClient;
54import android.net.Uri;
55import android.os.Handler;
56import android.os.IBinder;
57import android.os.Message;
58import android.os.PowerManager;
59import android.util.Log;
60import android.os.Process;
61
62import java.io.FileNotFoundException;
63import java.io.IOException;
64import java.io.InputStream;
65import java.util.ArrayList;
66
67/**
68 * Performs the background Bluetooth OPP transfer. It also starts thread to
69 * accept incoming OPP connection.
70 */
71
72public class BluetoothOppService extends Service {
73    private static final boolean D = Constants.DEBUG;
74    private static final boolean V = Constants.VERBOSE;
75
76    private boolean userAccepted = false;
77
78    private class BluetoothShareContentObserver extends ContentObserver {
79
80        public BluetoothShareContentObserver() {
81            super(new Handler());
82        }
83
84        @Override
85        public void onChange(boolean selfChange) {
86            if (V) Log.v(TAG, "ContentObserver received notification");
87            updateFromProvider();
88        }
89    }
90
91    private static final String TAG = "BtOpp Service";
92
93    /** Observer to get notified when the content observer's data changes */
94    private BluetoothShareContentObserver mObserver;
95
96    /** Class to handle Notification Manager updates */
97    private BluetoothOppNotification mNotifier;
98
99    private boolean mPendingUpdate;
100
101    private UpdateThread mUpdateThread;
102
103    private ArrayList<BluetoothOppShareInfo> mShares;
104
105    private ArrayList<BluetoothOppBatch> mBatchs;
106
107    private BluetoothOppTransfer mTransfer;
108
109    private BluetoothOppTransfer mServerTransfer;
110
111    private int mBatchId;
112
113    /**
114     * Array used when extracting strings from content provider
115     */
116    private CharArrayBuffer mOldChars;
117
118    /**
119     * Array used when extracting strings from content provider
120     */
121    private CharArrayBuffer mNewChars;
122
123    private BluetoothAdapter mAdapter;
124
125    private PowerManager mPowerManager;
126
127    private BluetoothOppRfcommListener mSocketListener;
128
129    private boolean mListenStarted = false;
130
131    private boolean mMediaScanInProgress;
132
133    private int mIncomingRetries = 0;
134
135    private ObexTransport mPendingConnection = null;
136
137    /*
138     * TODO No support for queue incoming from multiple devices.
139     * Make an array list of server session to support receiving queue from
140     * multiple devices
141     */
142    private BluetoothOppObexServerSession mServerSession;
143
144    @Override
145    public IBinder onBind(Intent arg0) {
146        throw new UnsupportedOperationException("Cannot bind to Bluetooth OPP Service");
147    }
148
149    @Override
150    public void onCreate() {
151        super.onCreate();
152        if (V) Log.v(TAG, "Service onCreate");
153        mAdapter = (BluetoothAdapter) getSystemService(Context.BLUETOOTH_SERVICE);
154        mSocketListener = new BluetoothOppRfcommListener(mAdapter);
155        mShares = Lists.newArrayList();
156        mBatchs = Lists.newArrayList();
157        mObserver = new BluetoothShareContentObserver();
158        getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver);
159        mBatchId = 1;
160        mNotifier = new BluetoothOppNotification(this);
161        mNotifier.mNotificationMgr.cancelAll();
162        mNotifier.updateNotification();
163        mNotifier.finishNotification();
164
165        trimDatabase();
166
167        IntentFilter filter = new IntentFilter(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION);
168        registerReceiver(mBluetoothIntentReceiver, filter);
169
170        synchronized (BluetoothOppService.this) {
171            if (mAdapter == null) {
172                Log.w(TAG, "Local BT device is not enabled");
173            } else {
174                startListenerDelayed();
175            }
176        }
177        if (V) BluetoothOppPreference.getInstance(this).dump();
178        updateFromProvider();
179    }
180
181    @Override
182    public void onStart(Intent intent, int startId) {
183        super.onStart(intent, startId);
184        if (V) Log.v(TAG, "Service onStart");
185
186        if (mAdapter == null) {
187            Log.w(TAG, "Local BT device is not enabled");
188        } else {
189            startListenerDelayed();
190        }
191        updateFromProvider();
192
193    }
194
195    private void startListenerDelayed() {
196        if (!mListenStarted) {
197            if (mAdapter.isEnabled()) {
198                if (V) Log.v(TAG, "Starting RfcommListener in 9 seconds");
199                mHandler.sendMessageDelayed(mHandler.obtainMessage(START_LISTENER), 9000);
200                mListenStarted = true;
201            }
202        }
203    }
204
205    private static final int START_LISTENER = 1;
206
207    private static final int MEDIA_SCANNED = 2;
208
209    private static final int MEDIA_SCANNED_FAILED = 3;
210
211    private static final int MSG_INCOMING_CONNECTION_RETRY = 4;
212
213    private Handler mHandler = new Handler() {
214        @Override
215        public void handleMessage(Message msg) {
216            switch (msg.what) {
217                case START_LISTENER:
218                    if (mAdapter.isEnabled()) {
219                        startSocketListener();
220                    }
221                    break;
222                case MEDIA_SCANNED:
223                    if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
224                                + msg.obj.toString());
225                    ContentValues updateValues = new ContentValues();
226                    Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
227                    updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
228                    updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
229                    updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType(
230                            Uri.parse(msg.obj.toString())));
231                    getContentResolver().update(contentUri, updateValues, null, null);
232                    synchronized (BluetoothOppService.this) {
233                        mMediaScanInProgress = false;
234                    }
235                    break;
236                case MEDIA_SCANNED_FAILED:
237                    Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
238                    ContentValues updateValues1 = new ContentValues();
239                    Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
240                    updateValues1.put(Constants.MEDIA_SCANNED,
241                            Constants.MEDIA_SCANNED_SCANNED_FAILED);
242                    getContentResolver().update(contentUri1, updateValues1, null, null);
243                    synchronized (BluetoothOppService.this) {
244                        mMediaScanInProgress = false;
245                    }
246                    break;
247                case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION:
248                    if (D) Log.d(TAG, "Get incoming connection");
249                    ObexTransport transport = (ObexTransport)msg.obj;
250                    /*
251                     * Strategy for incoming connections:
252                     * 1. If there is no ongoing transfer, no on-hold connection, start it
253                     * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
254                     * 3. If there is on-hold connection, reject directly
255                     */
256                    if (mBatchs.size() == 0 && mPendingConnection == null) {
257                        Log.i(TAG, "Start Obex Server");
258                        createServerSession(transport);
259                    } else {
260                        if (mPendingConnection != null) {
261                            Log.w(TAG, "OPP busy! Reject connection");
262                            try {
263                                transport.close();
264                            } catch (IOException e) {
265                                Log.e(TAG, "close tranport error");
266                            }
267                        } else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER){
268                            Log.i(TAG, "Start Obex Server in TCP DEBUG mode");
269                            createServerSession(transport);
270                        } else {
271                            Log.i(TAG, "OPP busy! Retry after 1 second");
272                            mIncomingRetries = mIncomingRetries + 1;
273                            mPendingConnection = transport;
274                            Message msg1 = Message.obtain(mHandler);
275                            msg1.what = MSG_INCOMING_CONNECTION_RETRY;
276                            mHandler.sendMessageDelayed(msg1, 1000);
277                        }
278                    }
279                    break;
280                case MSG_INCOMING_CONNECTION_RETRY:
281                    if (mBatchs.size() == 0) {
282                        Log.i(TAG, "Start Obex Server");
283                        createServerSession(mPendingConnection);
284                        mIncomingRetries = 0;
285                        mPendingConnection = null;
286                    } else {
287                        if (mIncomingRetries == 20) {
288                            Log.w(TAG, "Retried 20 seconds, reject connection");
289                            try {
290                                mPendingConnection.close();
291                            } catch (IOException e) {
292                                Log.e(TAG, "close tranport error");
293                            }
294                            mIncomingRetries = 0;
295                            mPendingConnection = null;
296                        } else {
297                            Log.i(TAG, "OPP busy! Retry after 1 second");
298                            mIncomingRetries = mIncomingRetries + 1;
299                            Message msg2 = Message.obtain(mHandler);
300                            msg2.what = MSG_INCOMING_CONNECTION_RETRY;
301                            mHandler.sendMessageDelayed(msg2, 1000);
302                        }
303                    }
304                    break;
305            }
306        }
307    };
308
309    private void startSocketListener() {
310
311        if (V) Log.v(TAG, "start RfcommListener");
312        mSocketListener.start(mHandler);
313        if (V) Log.v(TAG, "RfcommListener started");
314    }
315
316    @Override
317    public void onDestroy() {
318        if (V) Log.v(TAG, "Service onDestroy");
319        super.onDestroy();
320        mNotifier.finishNotification();
321        getContentResolver().unregisterContentObserver(mObserver);
322        unregisterReceiver(mBluetoothIntentReceiver);
323        mSocketListener.stop();
324    }
325
326    /* suppose we auto accept an incoming OPUSH connection */
327    private void createServerSession(ObexTransport transport) {
328        mServerSession = new BluetoothOppObexServerSession(this, transport);
329        mServerSession.preStart();
330        if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString()
331                    + " for incoming connection" + transport.toString());
332    }
333
334    private final BroadcastReceiver mBluetoothIntentReceiver = new BroadcastReceiver() {
335        @Override
336        public void onReceive(Context context, Intent intent) {
337            String action = intent.getAction();
338
339            if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) {
340                switch (intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, BluetoothError.ERROR)) {
341                    case BluetoothAdapter.BLUETOOTH_STATE_ON:
342                        if (V) Log.v(TAG,
343                                    "Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");
344                        startSocketListener();
345                        break;
346                    case BluetoothAdapter.BLUETOOTH_STATE_TURNING_OFF:
347                        if (V) Log.v(TAG, "Receiver DISABLED_ACTION ");
348                        mSocketListener.stop();
349                        mListenStarted = false;
350                        synchronized (BluetoothOppService.this) {
351                            if (mUpdateThread == null) {
352                                stopSelf();
353                            }
354                        }
355                        break;
356                }
357            }
358        }
359    };
360
361    private void updateFromProvider() {
362        synchronized (BluetoothOppService.this) {
363            mPendingUpdate = true;
364            if (mUpdateThread == null) {
365                mUpdateThread = new UpdateThread();
366                mUpdateThread.start();
367            }
368        }
369    }
370
371    private class UpdateThread extends Thread {
372        public UpdateThread() {
373            super("Bluetooth Share Service");
374        }
375
376        @Override
377        public void run() {
378            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
379
380            boolean keepService = false;
381            for (;;) {
382                synchronized (BluetoothOppService.this) {
383                    if (mUpdateThread != this) {
384                        throw new IllegalStateException(
385                                "multiple UpdateThreads in BluetoothOppService");
386                    }
387                    if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is "
388                                + keepService + " sListenStarted is " + mListenStarted);
389                    if (!mPendingUpdate) {
390                        mUpdateThread = null;
391                        if (!keepService && !mListenStarted) {
392                            stopSelf();
393                            break;
394                        }
395                        mNotifier.updateNotification();
396                        mNotifier.finishNotification();
397                        return;
398                    }
399                    mPendingUpdate = false;
400                }
401                Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null,
402                        null, BluetoothShare._ID);
403
404                if (cursor == null) {
405                    return;
406                }
407
408                cursor.moveToFirst();
409
410                int arrayPos = 0;
411
412                keepService = false;
413                boolean isAfterLast = cursor.isAfterLast();
414
415                int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
416                /*
417                 * Walk the cursor and the local array to keep them in sync. The
418                 * key to the algorithm is that the ids are unique and sorted
419                 * both in the cursor and in the array, so that they can be
420                 * processed in order in both sources at the same time: at each
421                 * step, both sources point to the lowest id that hasn't been
422                 * processed from that source, and the algorithm processes the
423                 * lowest id from those two possibilities. At each step: -If the
424                 * array contains an entry that's not in the cursor, remove the
425                 * entry, move to next entry in the array. -If the array
426                 * contains an entry that's in the cursor, nothing to do, move
427                 * to next cursor row and next array entry. -If the cursor
428                 * contains an entry that's not in the array, insert a new entry
429                 * in the array, move to next cursor row and next array entry.
430                 */
431                while (!isAfterLast || arrayPos < mShares.size()) {
432                    if (isAfterLast) {
433                        // We're beyond the end of the cursor but there's still
434                        // some
435                        // stuff in the local array, which can only be junk
436                        if (V) Log.v(TAG, "Array update: trimming " +
437                                mShares.get(arrayPos).mId + " @ " + arrayPos);
438
439                        if (shouldScanFile(arrayPos)) {
440                            scanFile(null, arrayPos);
441                        }
442                        deleteShare(arrayPos); // this advances in the array
443                    } else {
444                        int id = cursor.getInt(idColumn);
445
446                        if (arrayPos == mShares.size()) {
447                            insertShare(cursor, arrayPos);
448                            if (V) Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
449                            if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
450                                keepService = true;
451                            }
452                            if (visibleNotification(arrayPos)) {
453                                keepService = true;
454                            }
455                            if (needAction(arrayPos)) {
456                                keepService = true;
457                            }
458
459                            ++arrayPos;
460                            cursor.moveToNext();
461                            isAfterLast = cursor.isAfterLast();
462                        } else {
463                            int arrayId = mShares.get(arrayPos).mId;
464
465                            if (arrayId < id) {
466                                if (V) Log.v(TAG, "Array update: removing " + arrayId + " @ "
467                                            + arrayPos);
468                                if (shouldScanFile(arrayPos)) {
469                                    scanFile(null, arrayPos);
470                                }
471                                deleteShare(arrayPos);
472                            } else if (arrayId == id) {
473                                // This cursor row already exists in the stored
474                                // array
475                                updateShare(cursor, arrayPos, userAccepted);
476                                if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
477                                    keepService = true;
478                                }
479                                if (visibleNotification(arrayPos)) {
480                                    keepService = true;
481                                }
482                                if (needAction(arrayPos)) {
483                                    keepService = true;
484                                }
485
486                                ++arrayPos;
487                                cursor.moveToNext();
488                                isAfterLast = cursor.isAfterLast();
489                            } else {
490                                // This cursor entry didn't exist in the stored
491                                // array
492                                if (V) Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
493                                insertShare(cursor, arrayPos);
494
495                                if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
496                                    keepService = true;
497                                }
498                                if (visibleNotification(arrayPos)) {
499                                    keepService = true;
500                                }
501                                if (needAction(arrayPos)) {
502                                    keepService = true;
503                                }
504                                ++arrayPos;
505                                cursor.moveToNext();
506                                isAfterLast = cursor.isAfterLast();
507                            }
508                        }
509                    }
510                }
511
512                mNotifier.updateNotification();
513
514                cursor.close();
515            }
516        }
517
518    }
519
520    private void insertShare(Cursor cursor, int arrayPos) {
521        BluetoothOppShareInfo info = new BluetoothOppShareInfo(
522                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)),
523                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI)),
524                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
525                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
526                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
527                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
528                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
529                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
530                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
531                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
532                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
533                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
534                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
535                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
536
537        if (V) {
538            Log.v(TAG, "Service adding new entry");
539            Log.v(TAG, "ID      : " + info.mId);
540            // Log.v(TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
541            Log.v(TAG, "URI     : " + info.mUri);
542            Log.v(TAG, "HINT    : " + info.mHint);
543            Log.v(TAG, "FILENAME: " + info.mFilename);
544            Log.v(TAG, "MIMETYPE: " + info.mMimetype);
545            Log.v(TAG, "DIRECTION: " + info.mDirection);
546            Log.v(TAG, "DESTINAT: " + info.mDestination);
547            Log.v(TAG, "VISIBILI: " + info.mVisibility);
548            Log.v(TAG, "CONFIRM : " + info.mConfirm);
549            Log.v(TAG, "STATUS  : " + info.mStatus);
550            Log.v(TAG, "TOTAL   : " + info.mTotalBytes);
551            Log.v(TAG, "CURRENT : " + info.mCurrentBytes);
552            Log.v(TAG, "TIMESTAMP : " + info.mTimestamp);
553            Log.v(TAG, "SCANNED : " + info.mMediaScanned);
554        }
555
556        mShares.add(arrayPos, info);
557
558        /* Mark the info as failed if it's in invalid status */
559        if (info.isObsolete()) {
560            Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
561        }
562        /*
563         * Add info into a batch. The logic is
564         * 1) Only add valid and readyToStart info
565         * 2) If there is no batch, create a batch and insert this transfer into batch,
566         * then run the batch
567         * 3) If there is existing batch and timestamp match, insert transfer into batch
568         * 4) If there is existing batch and timestamp does not match, create a new batch and
569         * put in queue
570         */
571
572        if (info.isReadyToStart()) {
573            if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
574                /* check if the file exists */
575                try {
576                    InputStream i = getContentResolver().openInputStream(Uri.parse(info.mUri));
577                    i.close();
578                } catch (FileNotFoundException e) {
579                    Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
580                    Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
581                    return;
582                } catch (IOException ex) {
583                    Log.e(TAG, "IO error when close file for OUTBOUND info " + info.mId);
584                    return;
585                }
586            }
587            if (mBatchs.size() == 0) {
588                BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
589                newBatch.mId = mBatchId;
590                mBatchId++;
591                mBatchs.add(newBatch);
592                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
593                    if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
594                                + " for OUTBOUND info " + info.mId);
595                    mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
596                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
597                    if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
598                                + " for INBOUND info " + info.mId);
599                    mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
600                            mServerSession);
601                }
602
603                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
604                    if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId
605                                + " for info " + info.mId);
606                    mTransfer.start();
607                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
608                        && mServerTransfer != null) {
609                    if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
610                                + " for info " + info.mId);
611                    mServerTransfer.start();
612                }
613
614            } else {
615                int i = findBatchWithTimeStamp(info.mTimestamp);
616                if (i != -1) {
617                    if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch "
618                                + mBatchs.get(i).mId);
619                    mBatchs.get(i).addShare(info);
620                } else {
621                    // There is ongoing batch
622                    BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
623                    newBatch.mId = mBatchId;
624                    mBatchId++;
625                    mBatchs.add(newBatch);
626                    if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " +
627                            info.mId);
628                    if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
629                        // only allow  concurrent serverTransfer in debug mode
630                        if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
631                            if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " +
632                                    newBatch.mId + " for info " + info.mId);
633                            mServerTransfer = new BluetoothOppTransfer(this, mPowerManager,
634                                    newBatch, mServerSession);
635                            mServerTransfer.start();
636                        }
637                    }
638                }
639            }
640        }
641    }
642
643    private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) {
644        BluetoothOppShareInfo info = mShares.get(arrayPos);
645        int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
646
647        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
648        info.mUri = stringFromCursor(info.mUri, cursor, BluetoothShare.URI);
649        info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
650        info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
651        info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
652        info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
653        info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
654        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
655
656        boolean confirmed = false;
657        int newConfirm = cursor.getInt(cursor
658                .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
659
660        if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
661                && newVisibility != BluetoothShare.VISIBILITY_VISIBLE
662                && (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
663            mNotifier.mNotificationMgr.cancel(info.mId);
664        }
665
666        info.mVisibility = newVisibility;
667
668        if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
669                && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
670            confirmed = true;
671        }
672        info.mConfirm = cursor.getInt(cursor
673                .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
674        int newStatus = cursor.getInt(statusColumn);
675
676        if (!BluetoothShare.isStatusCompleted(info.mStatus)
677                && BluetoothShare.isStatusCompleted(newStatus)) {
678            mNotifier.mNotificationMgr.cancel(info.mId);
679        }
680
681        info.mStatus = newStatus;
682        info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
683        info.mCurrentBytes = cursor.getInt(cursor
684                .getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
685        info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
686        info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
687
688        if (confirmed) {
689            if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmed");
690            /* Inbounds transfer get user confirmation, so we start it */
691            int i = findBatchWithTimeStamp(info.mTimestamp);
692            BluetoothOppBatch batch = mBatchs.get(i);
693            if (batch.mId == mServerTransfer.getBatchId()) {
694                mServerTransfer.setConfirmed();
695            } //TODO need to think about else
696        }
697        int i = findBatchWithTimeStamp(info.mTimestamp);
698        if (i != -1) {
699            BluetoothOppBatch batch = mBatchs.get(i);
700            if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
701                    || batch.mStatus == Constants.BATCH_STATUS_FAILED) {
702                if (V) Log.v(TAG, "Batch " + batch.mId + " is finished");
703                if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
704                    if (mTransfer == null) {
705                        Log.e(TAG, "Unexpected error! mTransfer is null");
706                    } else if (batch.mId == mTransfer.getBatchId()) {
707                        mTransfer.stop();
708                    } else {
709                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
710                                + " doesn't match mTransfer id " + mTransfer.getBatchId());
711                    }
712                } else {
713                    if (mServerTransfer == null) {
714                        Log.e(TAG, "Unexpected error! mServerTransfer is null");
715                    } else if (batch.mId == mServerTransfer.getBatchId()) {
716                        mServerTransfer.stop();
717                    } else {
718                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
719                                + " doesn't match mServerTransfer id "
720                                + mServerTransfer.getBatchId());
721                    }
722                }
723                removeBatch(batch);
724            }
725        }
726    }
727
728    /**
729     * Removes the local copy of the info about a share.
730     */
731    private void deleteShare(int arrayPos) {
732        BluetoothOppShareInfo info = mShares.get(arrayPos);
733
734        /*
735         * Delete arrayPos from a batch. The logic is
736         * 1) Search existing batch for the info
737         * 2) cancel the batch
738         * 3) If the batch become empty delete the batch
739         */
740        int i = findBatchWithTimeStamp(info.mTimestamp);
741        if (i != -1) {
742            BluetoothOppBatch batch = mBatchs.get(i);
743            if (batch.hasShare(info)) {
744                if (V) Log.v(TAG, "Service cancel batch for share " + info.mId);
745                batch.cancelBatch();
746            }
747            if (batch.isEmpty()) {
748                if (V) Log.v(TAG, "Service remove batch  " + batch.mId);
749                removeBatch(batch);
750            }
751        }
752        mShares.remove(arrayPos);
753    }
754
755    private String stringFromCursor(String old, Cursor cursor, String column) {
756        int index = cursor.getColumnIndexOrThrow(column);
757        if (old == null) {
758            return cursor.getString(index);
759        }
760        if (mNewChars == null) {
761            mNewChars = new CharArrayBuffer(128);
762        }
763        cursor.copyStringToBuffer(index, mNewChars);
764        int length = mNewChars.sizeCopied;
765        if (length != old.length()) {
766            return cursor.getString(index);
767        }
768        if (mOldChars == null || mOldChars.sizeCopied < length) {
769            mOldChars = new CharArrayBuffer(length);
770        }
771        char[] oldArray = mOldChars.data;
772        char[] newArray = mNewChars.data;
773        old.getChars(0, length, oldArray, 0);
774        for (int i = length - 1; i >= 0; --i) {
775            if (oldArray[i] != newArray[i]) {
776                return new String(newArray, 0, length);
777            }
778        }
779        return old;
780    }
781
782    private int findBatchWithTimeStamp(long timestamp) {
783        for (int i = mBatchs.size() - 1; i >= 0; i--) {
784            if (mBatchs.get(i).mTimestamp == timestamp) {
785                return i;
786            }
787        }
788        return -1;
789    }
790
791    private void removeBatch(BluetoothOppBatch batch) {
792        if (V) Log.v(TAG, "Remove batch " + batch.mId);
793        mBatchs.remove(batch);
794        if (mBatchs.size() > 0) {
795            for (int i = 0; i < mBatchs.size(); i++) {
796                // we have a running batch
797                if (mBatchs.get(i).mStatus == Constants.BATCH_STATUS_RUNNING) {
798                    return;
799                } else {
800                    // Pending batch for inbound transfer is not supported
801                    // just finish a transfer, start pending outbound transfer
802                    if (mBatchs.get(i).mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
803                        if (V) Log.v(TAG, "Start pending outbound batch " + mBatchs.get(i).mId);
804                        mTransfer = new BluetoothOppTransfer(this, mPowerManager, mBatchs.get(i));
805                        mTransfer.start();
806                        return;
807                    } else {
808                        // have pending inbound transfer, should not happen
809                        Log.e(TAG, "Should not happen");
810                    }
811                }
812            }
813        }
814    }
815
816    private boolean needAction(int arrayPos) {
817        BluetoothOppShareInfo info = mShares.get(arrayPos);
818        if (BluetoothShare.isStatusCompleted(info.mStatus)) {
819            return false;
820        }
821        return true;
822    }
823
824    private boolean visibleNotification(int arrayPos) {
825        BluetoothOppShareInfo info = mShares.get(arrayPos);
826        return info.hasCompletionNotification();
827    }
828
829    private boolean scanFile(Cursor cursor, int arrayPos) {
830        BluetoothOppShareInfo info = mShares.get(arrayPos);
831        synchronized (BluetoothOppService.this) {
832            if (D) Log.d(TAG, "Scanning file " + info.mFilename);
833            if (!mMediaScanInProgress) {
834                mMediaScanInProgress = true;
835                new MediaScannerNotifier(this, info, mHandler);
836                return true;
837            } else {
838                return false;
839            }
840        }
841    }
842
843    private boolean shouldScanFile(int arrayPos) {
844        BluetoothOppShareInfo info = mShares.get(arrayPos);
845        return BluetoothShare.isStatusSuccess(info.mStatus)
846                && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned;
847    }
848
849    private void trimDatabase() {
850        Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, new String[] {
851            BluetoothShare._ID
852        }, BluetoothShare.STATUS + " >= '200'", null, BluetoothShare._ID);
853        if (cursor == null) {
854            // This isn't good - if we can't do basic queries in our database,
855            // nothing's gonna work
856            Log.e(TAG, "null cursor in trimDatabase");
857            return;
858        }
859        if (cursor.moveToFirst()) {
860            int numDelete = cursor.getCount() - Constants.MAX_RECORDS_IN_DATABASE;
861            int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
862            while (numDelete > 0) {
863                getContentResolver().delete(
864                        ContentUris.withAppendedId(BluetoothShare.CONTENT_URI, cursor
865                                .getLong(columnId)), null, null);
866                if (!cursor.moveToNext()) {
867                    break;
868                }
869                numDelete--;
870            }
871        }
872        cursor.close();
873    }
874
875    private static class MediaScannerNotifier implements MediaScannerConnectionClient {
876
877        private MediaScannerConnection mConnection;
878
879        private BluetoothOppShareInfo mInfo;
880
881        private Context mContext;
882
883        private Handler mCallback;
884
885        public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
886            mContext = context;
887            mInfo = info;
888            mCallback = handler;
889            mConnection = new MediaScannerConnection(mContext, this);
890            if (V) Log.v(TAG, "Connecting to MediaScannerConnection ");
891            mConnection.connect();
892        }
893
894        public void onMediaScannerConnected() {
895            if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
896            mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
897        }
898
899        public void onScanCompleted(String path, Uri uri) {
900            try {
901                if (V) {
902                    Log.v(TAG, "MediaScannerConnection onScanCompleted");
903                    Log.v(TAG, "MediaScannerConnection path is " + path);
904                    Log.v(TAG, "MediaScannerConnection Uri is " + uri);
905                }
906                if (uri != null) {
907                    Message msg = Message.obtain();
908                    msg.setTarget(mCallback);
909                    msg.what = MEDIA_SCANNED;
910                    msg.arg1 = mInfo.mId;
911                    msg.obj = uri;
912                    msg.sendToTarget();
913                } else {
914                    Message msg = Message.obtain();
915                    msg.setTarget(mCallback);
916                    msg.what = MEDIA_SCANNED_FAILED;
917                    msg.arg1 = mInfo.mId;
918                    msg.sendToTarget();
919                }
920            } catch (Exception ex) {
921                Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
922            } finally {
923                if (V) Log.v(TAG, "MediaScannerConnection disconnect");
924                mConnection.disconnect();
925            }
926        }
927    }
928}
929