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