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