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