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