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