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