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