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