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