BluetoothOppService.java revision 3a88b20fcd71e42451e402d27374b19eeb2ff0da
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.content.BroadcastReceiver;
42import android.content.ContentUris;
43import android.content.ContentValues;
44import android.content.Context;
45import android.content.Intent;
46import android.content.IntentFilter;
47import android.database.CharArrayBuffer;
48import android.database.ContentObserver;
49import android.database.Cursor;
50import android.media.MediaScannerConnection;
51import android.media.MediaScannerConnection.MediaScannerConnectionClient;
52import android.net.Uri;
53import android.os.Handler;
54import android.os.IBinder;
55import android.os.Message;
56import android.os.PowerManager;
57import android.util.Log;
58import android.os.Process;
59
60import java.io.FileNotFoundException;
61import java.io.IOException;
62import java.io.InputStream;
63import java.util.ArrayList;
64
65/**
66 * Performs the background Bluetooth OPP transfer. It also starts thread to
67 * accept incoming OPP connection.
68 */
69
70public class BluetoothOppService extends Service {
71    private static final boolean D = Constants.DEBUG;
72    private static final boolean V = Constants.VERBOSE;
73
74    private boolean userAccepted = false;
75
76    private class BluetoothShareContentObserver extends ContentObserver {
77
78        public BluetoothShareContentObserver() {
79            super(new Handler());
80        }
81
82        @Override
83        public void onChange(boolean selfChange) {
84            if (V) Log.v(TAG, "ContentObserver received notification");
85            updateFromProvider();
86        }
87    }
88
89    private static final String TAG = "BtOpp Service";
90
91    /** Observer to get notified when the content observer's data changes */
92    private BluetoothShareContentObserver mObserver;
93
94    /** Class to handle Notification Manager updates */
95    private BluetoothOppNotification mNotifier;
96
97    private boolean mPendingUpdate;
98
99    private UpdateThread mUpdateThread;
100
101    private ArrayList<BluetoothOppShareInfo> mShares;
102
103    private ArrayList<BluetoothOppBatch> mBatchs;
104
105    private BluetoothOppTransfer mTransfer;
106
107    private BluetoothOppTransfer mServerTransfer;
108
109    private int mBatchId;
110
111    /**
112     * Array used when extracting strings from content provider
113     */
114    private CharArrayBuffer mOldChars;
115
116    /**
117     * Array used when extracting strings from content provider
118     */
119    private CharArrayBuffer mNewChars;
120
121    private BluetoothAdapter mAdapter;
122
123    private PowerManager mPowerManager;
124
125    private BluetoothOppRfcommListener mSocketListener;
126
127    private boolean mListenStarted = false;
128
129    private boolean mMediaScanInProgress;
130
131    private int mIncomingRetries = 0;
132
133    private ObexTransport mPendingConnection = null;
134
135    /*
136     * TODO No support for queue incoming from multiple devices.
137     * Make an array list of server session to support receiving queue from
138     * multiple devices
139     */
140    private BluetoothOppObexServerSession mServerSession;
141
142    @Override
143    public IBinder onBind(Intent arg0) {
144        throw new UnsupportedOperationException("Cannot bind to Bluetooth OPP Service");
145    }
146
147    @Override
148    public void onCreate() {
149        super.onCreate();
150        if (V) Log.v(TAG, "Service onCreate");
151        mAdapter = BluetoothAdapter.getDefaultAdapter();
152        mSocketListener = new BluetoothOppRfcommListener(mAdapter);
153        mShares = Lists.newArrayList();
154        mBatchs = Lists.newArrayList();
155        mObserver = new BluetoothShareContentObserver();
156        getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver);
157        mBatchId = 1;
158        mNotifier = new BluetoothOppNotification(this);
159        mNotifier.mNotificationMgr.cancelAll();
160        mNotifier.updateNotification();
161        mNotifier.finishNotification();
162
163        trimDatabase();
164
165        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
166        registerReceiver(mBluetoothReceiver, filter);
167
168        synchronized (BluetoothOppService.this) {
169            if (mAdapter == null) {
170                Log.w(TAG, "Local BT device is not enabled");
171            } else {
172                startListenerDelayed();
173            }
174        }
175        if (V) BluetoothOppPreference.getInstance(this).dump();
176        updateFromProvider();
177    }
178
179    @Override
180    public int onStartCommand(Intent intent, int flags, int startId) {
181        if (V) Log.v(TAG, "Service onStartCommand");
182        int retCode = super.onStartCommand(intent, flags, startId);
183        if (retCode == START_STICKY) {
184            if (mAdapter == null) {
185                Log.w(TAG, "Local BT device is not enabled");
186            } else {
187                startListenerDelayed();
188            }
189            updateFromProvider();
190        }
191        return retCode;
192    }
193
194    private void startListenerDelayed() {
195        if (!mListenStarted) {
196            if (mAdapter.isEnabled()) {
197                if (V) Log.v(TAG, "Starting RfcommListener in 9 seconds");
198                mHandler.sendMessageDelayed(mHandler.obtainMessage(START_LISTENER), 9000);
199                mListenStarted = true;
200            }
201        }
202    }
203
204    private static final int START_LISTENER = 1;
205
206    private static final int MEDIA_SCANNED = 2;
207
208    private static final int MEDIA_SCANNED_FAILED = 3;
209
210    private static final int MSG_INCOMING_CONNECTION_RETRY = 4;
211
212    private Handler mHandler = new Handler() {
213        @Override
214        public void handleMessage(Message msg) {
215            switch (msg.what) {
216                case START_LISTENER:
217                    if (mAdapter.isEnabled()) {
218                        startSocketListener();
219                    }
220                    break;
221                case MEDIA_SCANNED:
222                    if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
223                                + msg.obj.toString());
224                    ContentValues updateValues = new ContentValues();
225                    Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
226                    updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
227                    updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
228                    updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType(
229                            Uri.parse(msg.obj.toString())));
230                    getContentResolver().update(contentUri, updateValues, null, null);
231                    synchronized (BluetoothOppService.this) {
232                        mMediaScanInProgress = false;
233                    }
234                    break;
235                case MEDIA_SCANNED_FAILED:
236                    Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
237                    ContentValues updateValues1 = new ContentValues();
238                    Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
239                    updateValues1.put(Constants.MEDIA_SCANNED,
240                            Constants.MEDIA_SCANNED_SCANNED_FAILED);
241                    getContentResolver().update(contentUri1, updateValues1, null, null);
242                    synchronized (BluetoothOppService.this) {
243                        mMediaScanInProgress = false;
244                    }
245                    break;
246                case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION:
247                    if (D) Log.d(TAG, "Get incoming connection");
248                    ObexTransport transport = (ObexTransport)msg.obj;
249                    /*
250                     * Strategy for incoming connections:
251                     * 1. If there is no ongoing transfer, no on-hold connection, start it
252                     * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
253                     * 3. If there is on-hold connection, reject directly
254                     */
255                    if (mBatchs.size() == 0 && mPendingConnection == null) {
256                        Log.i(TAG, "Start Obex Server");
257                        createServerSession(transport);
258                    } else {
259                        if (mPendingConnection != null) {
260                            Log.w(TAG, "OPP busy! Reject connection");
261                            try {
262                                transport.close();
263                            } catch (IOException e) {
264                                Log.e(TAG, "close tranport error");
265                            }
266                        } else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER){
267                            Log.i(TAG, "Start Obex Server in TCP DEBUG mode");
268                            createServerSession(transport);
269                        } else {
270                            Log.i(TAG, "OPP busy! Retry after 1 second");
271                            mIncomingRetries = mIncomingRetries + 1;
272                            mPendingConnection = transport;
273                            Message msg1 = Message.obtain(mHandler);
274                            msg1.what = MSG_INCOMING_CONNECTION_RETRY;
275                            mHandler.sendMessageDelayed(msg1, 1000);
276                        }
277                    }
278                    break;
279                case MSG_INCOMING_CONNECTION_RETRY:
280                    if (mBatchs.size() == 0) {
281                        Log.i(TAG, "Start Obex Server");
282                        createServerSession(mPendingConnection);
283                        mIncomingRetries = 0;
284                        mPendingConnection = null;
285                    } else {
286                        if (mIncomingRetries == 20) {
287                            Log.w(TAG, "Retried 20 seconds, reject connection");
288                            try {
289                                mPendingConnection.close();
290                            } catch (IOException e) {
291                                Log.e(TAG, "close tranport error");
292                            }
293                            mIncomingRetries = 0;
294                            mPendingConnection = null;
295                        } else {
296                            Log.i(TAG, "OPP busy! Retry after 1 second");
297                            mIncomingRetries = mIncomingRetries + 1;
298                            Message msg2 = Message.obtain(mHandler);
299                            msg2.what = MSG_INCOMING_CONNECTION_RETRY;
300                            mHandler.sendMessageDelayed(msg2, 1000);
301                        }
302                    }
303                    break;
304            }
305        }
306    };
307
308    private void startSocketListener() {
309
310        if (V) Log.v(TAG, "start RfcommListener");
311        mSocketListener.start(mHandler);
312        if (V) Log.v(TAG, "RfcommListener started");
313    }
314
315    @Override
316    public void onDestroy() {
317        if (V) Log.v(TAG, "Service onDestroy");
318        super.onDestroy();
319        mNotifier.finishNotification();
320        getContentResolver().unregisterContentObserver(mObserver);
321        unregisterReceiver(mBluetoothReceiver);
322        mSocketListener.stop();
323    }
324
325    /* suppose we auto accept an incoming OPUSH connection */
326    private void createServerSession(ObexTransport transport) {
327        mServerSession = new BluetoothOppObexServerSession(this, transport);
328        mServerSession.preStart();
329        if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString()
330                    + " for incoming connection" + transport.toString());
331    }
332
333    private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
334        @Override
335        public void onReceive(Context context, Intent intent) {
336            String action = intent.getAction();
337
338            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
339                switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
340                    case BluetoothAdapter.STATE_ON:
341                        if (V) Log.v(TAG,
342                                    "Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");
343                        startSocketListener();
344                        break;
345                    case BluetoothAdapter.STATE_TURNING_OFF:
346                        if (V) Log.v(TAG, "Receiver DISABLED_ACTION ");
347                        mSocketListener.stop();
348                        mListenStarted = false;
349                        synchronized (BluetoothOppService.this) {
350                            if (mUpdateThread == null) {
351                                stopSelf();
352                            }
353                        }
354                        break;
355                }
356            }
357        }
358    };
359
360    private void updateFromProvider() {
361        synchronized (BluetoothOppService.this) {
362            mPendingUpdate = true;
363            if (mUpdateThread == null) {
364                mUpdateThread = new UpdateThread();
365                mUpdateThread.start();
366            }
367        }
368    }
369
370    private class UpdateThread extends Thread {
371        public UpdateThread() {
372            super("Bluetooth Share Service");
373        }
374
375        @Override
376        public void run() {
377            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
378
379            boolean keepService = false;
380            for (;;) {
381                synchronized (BluetoothOppService.this) {
382                    if (mUpdateThread != this) {
383                        throw new IllegalStateException(
384                                "multiple UpdateThreads in BluetoothOppService");
385                    }
386                    if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is "
387                                + keepService + " sListenStarted is " + mListenStarted);
388                    if (!mPendingUpdate) {
389                        mUpdateThread = null;
390                        if (!keepService && !mListenStarted) {
391                            stopSelf();
392                            break;
393                        }
394                        mNotifier.updateNotification();
395                        mNotifier.finishNotification();
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                try {
575                    InputStream i = getContentResolver().openInputStream(Uri.parse(info.mUri));
576                    i.close();
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 (IOException ex) {
582                    Log.e(TAG, "IO error when close file for OUTBOUND info " + info.mId);
583                    return;
584                }
585            }
586            if (mBatchs.size() == 0) {
587                BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
588                newBatch.mId = mBatchId;
589                mBatchId++;
590                mBatchs.add(newBatch);
591                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
592                    if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
593                                + " for OUTBOUND info " + info.mId);
594                    mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
595                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
596                    if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
597                                + " for INBOUND info " + info.mId);
598                    mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
599                            mServerSession);
600                }
601
602                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
603                    if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId
604                                + " for info " + info.mId);
605                    mTransfer.start();
606                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
607                        && mServerTransfer != null) {
608                    if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
609                                + " for info " + info.mId);
610                    mServerTransfer.start();
611                }
612
613            } else {
614                int i = findBatchWithTimeStamp(info.mTimestamp);
615                if (i != -1) {
616                    if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch "
617                                + mBatchs.get(i).mId);
618                    mBatchs.get(i).addShare(info);
619                } else {
620                    // There is ongoing batch
621                    BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
622                    newBatch.mId = mBatchId;
623                    mBatchId++;
624                    mBatchs.add(newBatch);
625                    if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " +
626                            info.mId);
627                    if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
628                        // only allow  concurrent serverTransfer in debug mode
629                        if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
630                            if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " +
631                                    newBatch.mId + " for info " + info.mId);
632                            mServerTransfer = new BluetoothOppTransfer(this, mPowerManager,
633                                    newBatch, mServerSession);
634                            mServerTransfer.start();
635                        }
636                    }
637                }
638            }
639        }
640    }
641
642    private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) {
643        BluetoothOppShareInfo info = mShares.get(arrayPos);
644        int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
645
646        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
647        info.mUri = stringFromCursor(info.mUri, cursor, BluetoothShare.URI);
648        info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
649        info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
650        info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
651        info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
652        info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
653        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
654
655        boolean confirmed = false;
656        int newConfirm = cursor.getInt(cursor
657                .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
658
659        if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
660                && newVisibility != BluetoothShare.VISIBILITY_VISIBLE
661                && (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
662            mNotifier.mNotificationMgr.cancel(info.mId);
663        }
664
665        info.mVisibility = newVisibility;
666
667        if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
668                && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
669            confirmed = true;
670        }
671        info.mConfirm = cursor.getInt(cursor
672                .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
673        int newStatus = cursor.getInt(statusColumn);
674
675        if (!BluetoothShare.isStatusCompleted(info.mStatus)
676                && BluetoothShare.isStatusCompleted(newStatus)) {
677            mNotifier.mNotificationMgr.cancel(info.mId);
678        }
679
680        info.mStatus = newStatus;
681        info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
682        info.mCurrentBytes = cursor.getInt(cursor
683                .getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
684        info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
685        info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
686
687        if (confirmed) {
688            if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmed");
689            /* Inbounds transfer get user confirmation, so we start it */
690            int i = findBatchWithTimeStamp(info.mTimestamp);
691            if (i != -1) {
692                BluetoothOppBatch batch = mBatchs.get(i);
693                if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) {
694                    mServerTransfer.setConfirmed();
695                } //TODO need to think about else
696            }
697        }
698        int i = findBatchWithTimeStamp(info.mTimestamp);
699        if (i != -1) {
700            BluetoothOppBatch batch = mBatchs.get(i);
701            if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
702                    || batch.mStatus == Constants.BATCH_STATUS_FAILED) {
703                if (V) Log.v(TAG, "Batch " + batch.mId + " is finished");
704                if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
705                    if (mTransfer == null) {
706                        Log.e(TAG, "Unexpected error! mTransfer is null");
707                    } else if (batch.mId == mTransfer.getBatchId()) {
708                        mTransfer.stop();
709                    } else {
710                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
711                                + " doesn't match mTransfer id " + mTransfer.getBatchId());
712                    }
713                    mTransfer = null;
714                } else {
715                    if (mServerTransfer == null) {
716                        Log.e(TAG, "Unexpected error! mServerTransfer is null");
717                    } else if (batch.mId == mServerTransfer.getBatchId()) {
718                        mServerTransfer.stop();
719                    } else {
720                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
721                                + " doesn't match mServerTransfer id "
722                                + mServerTransfer.getBatchId());
723                    }
724                    mServerTransfer = null;
725                }
726                removeBatch(batch);
727            }
728        }
729    }
730
731    /**
732     * Removes the local copy of the info about a share.
733     */
734    private void deleteShare(int arrayPos) {
735        BluetoothOppShareInfo info = mShares.get(arrayPos);
736
737        /*
738         * Delete arrayPos from a batch. The logic is
739         * 1) Search existing batch for the info
740         * 2) cancel the batch
741         * 3) If the batch become empty delete the batch
742         */
743        int i = findBatchWithTimeStamp(info.mTimestamp);
744        if (i != -1) {
745            BluetoothOppBatch batch = mBatchs.get(i);
746            if (batch.hasShare(info)) {
747                if (V) Log.v(TAG, "Service cancel batch for share " + info.mId);
748                batch.cancelBatch();
749            }
750            if (batch.isEmpty()) {
751                if (V) Log.v(TAG, "Service remove batch  " + batch.mId);
752                removeBatch(batch);
753            }
754        }
755        mShares.remove(arrayPos);
756    }
757
758    private String stringFromCursor(String old, Cursor cursor, String column) {
759        int index = cursor.getColumnIndexOrThrow(column);
760        if (old == null) {
761            return cursor.getString(index);
762        }
763        if (mNewChars == null) {
764            mNewChars = new CharArrayBuffer(128);
765        }
766        cursor.copyStringToBuffer(index, mNewChars);
767        int length = mNewChars.sizeCopied;
768        if (length != old.length()) {
769            return cursor.getString(index);
770        }
771        if (mOldChars == null || mOldChars.sizeCopied < length) {
772            mOldChars = new CharArrayBuffer(length);
773        }
774        char[] oldArray = mOldChars.data;
775        char[] newArray = mNewChars.data;
776        old.getChars(0, length, oldArray, 0);
777        for (int i = length - 1; i >= 0; --i) {
778            if (oldArray[i] != newArray[i]) {
779                return new String(newArray, 0, length);
780            }
781        }
782        return old;
783    }
784
785    private int findBatchWithTimeStamp(long timestamp) {
786        for (int i = mBatchs.size() - 1; i >= 0; i--) {
787            if (mBatchs.get(i).mTimestamp == timestamp) {
788                return i;
789            }
790        }
791        return -1;
792    }
793
794    private void removeBatch(BluetoothOppBatch batch) {
795        if (V) Log.v(TAG, "Remove batch " + batch.mId);
796        mBatchs.remove(batch);
797        BluetoothOppBatch nextBatch;
798        if (mBatchs.size() > 0) {
799            for (int i = 0; i < mBatchs.size(); i++) {
800                // we have a running batch
801                nextBatch = mBatchs.get(i);
802                if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) {
803                    return;
804                } else {
805                    // just finish a transfer, start pending outbound transfer
806                    if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
807                        if (V) Log.v(TAG, "Start pending outbound batch " + nextBatch.mId);
808                        mTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch);
809                        mTransfer.start();
810                        return;
811                    } else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND
812                            && mServerSession != null) {
813                        // have to support pending inbound transfer
814                        // if an outbound transfer and incoming socket happens together
815                        if (V) Log.v(TAG, "Start pending inbound batch " + nextBatch.mId);
816                        mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch,
817                                                                   mServerSession);
818                        mServerTransfer.start();
819                        if (nextBatch.getPendingShare().mConfirm ==
820                                BluetoothShare.USER_CONFIRMATION_CONFIRMED) {
821                            mServerTransfer.setConfirmed();
822                        }
823                        return;
824                    }
825                }
826            }
827        }
828    }
829
830    private boolean needAction(int arrayPos) {
831        BluetoothOppShareInfo info = mShares.get(arrayPos);
832        if (BluetoothShare.isStatusCompleted(info.mStatus)) {
833            return false;
834        }
835        return true;
836    }
837
838    private boolean visibleNotification(int arrayPos) {
839        BluetoothOppShareInfo info = mShares.get(arrayPos);
840        return info.hasCompletionNotification();
841    }
842
843    private boolean scanFile(Cursor cursor, int arrayPos) {
844        BluetoothOppShareInfo info = mShares.get(arrayPos);
845        synchronized (BluetoothOppService.this) {
846            if (D) Log.d(TAG, "Scanning file " + info.mFilename);
847            if (!mMediaScanInProgress) {
848                mMediaScanInProgress = true;
849                new MediaScannerNotifier(this, info, mHandler);
850                return true;
851            } else {
852                return false;
853            }
854        }
855    }
856
857    private boolean shouldScanFile(int arrayPos) {
858        BluetoothOppShareInfo info = mShares.get(arrayPos);
859        return BluetoothShare.isStatusSuccess(info.mStatus)
860                && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned;
861    }
862
863    private void trimDatabase() {
864        Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, new String[] {
865            BluetoothShare._ID
866        }, BluetoothShare.STATUS + " >= '200'", null, BluetoothShare._ID);
867        if (cursor == null) {
868            // This isn't good - if we can't do basic queries in our database,
869            // nothing's gonna work
870            Log.e(TAG, "null cursor in trimDatabase");
871            return;
872        }
873        if (cursor.moveToFirst()) {
874            int numDelete = cursor.getCount() - Constants.MAX_RECORDS_IN_DATABASE;
875            int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
876            while (numDelete > 0) {
877                getContentResolver().delete(
878                        ContentUris.withAppendedId(BluetoothShare.CONTENT_URI, cursor
879                                .getLong(columnId)), null, null);
880                if (!cursor.moveToNext()) {
881                    break;
882                }
883                numDelete--;
884            }
885        }
886        cursor.close();
887    }
888
889    private static class MediaScannerNotifier implements MediaScannerConnectionClient {
890
891        private MediaScannerConnection mConnection;
892
893        private BluetoothOppShareInfo mInfo;
894
895        private Context mContext;
896
897        private Handler mCallback;
898
899        public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
900            mContext = context;
901            mInfo = info;
902            mCallback = handler;
903            mConnection = new MediaScannerConnection(mContext, this);
904            if (V) Log.v(TAG, "Connecting to MediaScannerConnection ");
905            mConnection.connect();
906        }
907
908        public void onMediaScannerConnected() {
909            if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
910            mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
911        }
912
913        public void onScanCompleted(String path, Uri uri) {
914            try {
915                if (V) {
916                    Log.v(TAG, "MediaScannerConnection onScanCompleted");
917                    Log.v(TAG, "MediaScannerConnection path is " + path);
918                    Log.v(TAG, "MediaScannerConnection Uri is " + uri);
919                }
920                if (uri != null) {
921                    Message msg = Message.obtain();
922                    msg.setTarget(mCallback);
923                    msg.what = MEDIA_SCANNED;
924                    msg.arg1 = mInfo.mId;
925                    msg.obj = uri;
926                    msg.sendToTarget();
927                } else {
928                    Message msg = Message.obtain();
929                    msg.setTarget(mCallback);
930                    msg.what = MEDIA_SCANNED_FAILED;
931                    msg.arg1 = mInfo.mId;
932                    msg.sendToTarget();
933                }
934            } catch (Exception ex) {
935                Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
936            } finally {
937                if (V) Log.v(TAG, "MediaScannerConnection disconnect");
938                mConnection.disconnect();
939            }
940        }
941    }
942}
943