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