BluetoothOppService.java revision bd90909c4ef180602ac088758ffdc13d37d24629
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 android.bluetooth.BluetoothAdapter;
36import android.bluetooth.BluetoothDevice;
37import android.bluetooth.BluetoothDevicePicker;
38import android.bluetooth.BluetoothSocket;
39import android.content.BroadcastReceiver;
40import android.content.ContentResolver;
41import android.content.ContentValues;
42import android.content.Context;
43import android.content.Intent;
44import android.content.IntentFilter;
45import android.database.CharArrayBuffer;
46import android.database.ContentObserver;
47import android.database.Cursor;
48import android.media.MediaScannerConnection;
49import android.media.MediaScannerConnection.MediaScannerConnectionClient;
50import android.net.Uri;
51import android.os.Binder;
52import android.os.Handler;
53import android.os.Message;
54import android.os.Process;
55import android.support.annotation.VisibleForTesting;
56import android.util.Log;
57
58import com.android.bluetooth.BluetoothObexTransport;
59import com.android.bluetooth.IObexConnectionHandler;
60import com.android.bluetooth.ObexServerSockets;
61import com.android.bluetooth.btservice.ProfileService;
62import com.android.bluetooth.sdp.SdpManager;
63
64import com.google.android.collect.Lists;
65
66import java.io.IOException;
67import java.text.SimpleDateFormat;
68import java.util.ArrayList;
69import java.util.Date;
70import java.util.Locale;
71
72import javax.obex.ObexTransport;
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[] SUPPORTED_OPP_FORMAT = {
84            0x01 /* vCard 2.1 */,
85            0x02 /* vCard 3.0 */,
86            0x03 /* vCal 1.0 */,
87            0x04 /* iCal 2.0 */,
88            (byte) 0xFF /* Any type of object */
89    };
90
91    private class BluetoothShareContentObserver extends ContentObserver {
92
93        BluetoothShareContentObserver() {
94            super(new Handler());
95        }
96
97        @Override
98        public void onChange(boolean selfChange) {
99            if (V) {
100                Log.v(TAG, "ContentObserver received notification");
101            }
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> mBatches;
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 boolean mListenStarted;
138
139    private boolean mMediaScanInProgress;
140
141    private int mIncomingRetries;
142
143    private ObexTransport mPendingConnection;
144
145    private int mOppSdpHandle = -1;
146
147    boolean mAcceptNewConnections;
148
149    private static final String INVISIBLE =
150            BluetoothShare.VISIBILITY + "=" + BluetoothShare.VISIBILITY_HIDDEN;
151
152    private static final String WHERE_INBOUND_SUCCESS =
153            BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND + " AND "
154                    + BluetoothShare.STATUS + "=" + BluetoothShare.STATUS_SUCCESS + " AND "
155                    + INVISIBLE;
156
157    private static final String WHERE_CONFIRM_PENDING_INBOUND =
158            BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND + " AND "
159                    + BluetoothShare.USER_CONFIRMATION + "="
160                    + BluetoothShare.USER_CONFIRMATION_PENDING;
161
162    private static final String WHERE_INVISIBLE_UNCONFIRMED =
163            "(" + BluetoothShare.STATUS + ">=" + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE
164                    + ") OR (" + WHERE_CONFIRM_PENDING_INBOUND + ")";
165
166    private static BluetoothOppService sBluetoothOppService;
167
168    /*
169     * TODO No support for queue incoming from multiple devices.
170     * Make an array list of server session to support receiving queue from
171     * multiple devices
172     */
173    private BluetoothOppObexServerSession mServerSession;
174
175    @Override
176    protected IProfileServiceBinder initBinder() {
177        return new OppBinder(this);
178    }
179
180    private static class OppBinder extends Binder implements IProfileServiceBinder {
181
182        OppBinder(BluetoothOppService service) {
183        }
184
185        @Override
186        public void cleanup() {
187        }
188    }
189
190    @Override
191    protected void create() {
192        if (V) {
193            Log.v(TAG, "onCreate");
194        }
195        mShares = Lists.newArrayList();
196        mBatches = Lists.newArrayList();
197        mBatchId = 1;
198        final ContentResolver contentResolver = getContentResolver();
199        new Thread("trimDatabase") {
200            @Override
201            public void run() {
202                trimDatabase(contentResolver);
203            }
204        }.start();
205
206        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
207        registerReceiver(mBluetoothReceiver, filter);
208
209        synchronized (BluetoothOppService.this) {
210            if (mAdapter == null) {
211                Log.w(TAG, "Local BT device is not enabled");
212            }
213        }
214        if (V) {
215            BluetoothOppPreference preference = BluetoothOppPreference.getInstance(this);
216            if (preference != null) {
217                preference.dump();
218            } else {
219                Log.w(TAG, "BluetoothOppPreference.getInstance returned null.");
220            }
221        }
222    }
223
224    @Override
225    public boolean start() {
226        if (V) {
227            Log.v(TAG, "start()");
228        }
229        mObserver = new BluetoothShareContentObserver();
230        getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver);
231        mNotifier = new BluetoothOppNotification(this);
232        mNotifier.mNotificationMgr.cancelAll();
233        mNotifier.updateNotification();
234        updateFromProvider();
235        setBluetoothOppService(this);
236        return true;
237    }
238
239    @Override
240    public boolean stop() {
241        setBluetoothOppService(null);
242        mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
243        return true;
244    }
245
246    private void startListener() {
247        if (!mListenStarted) {
248            if (mAdapter.isEnabled()) {
249                if (V) {
250                    Log.v(TAG, "Starting RfcommListener");
251                }
252                mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER));
253                mListenStarted = true;
254            }
255        }
256    }
257
258    @Override
259    public void dump(StringBuilder sb) {
260        super.dump(sb);
261        if (mShares.size() > 0) {
262            println(sb, "Shares:");
263            for (BluetoothOppShareInfo info : mShares) {
264                String dir = info.mDirection == BluetoothShare.DIRECTION_OUTBOUND ? " -> " : " <- ";
265                SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss", Locale.US);
266                Date date = new Date(info.mTimestamp);
267                println(sb, "  " + format.format(date) + dir + info.mCurrentBytes + "/"
268                        + info.mTotalBytes);
269            }
270        }
271    }
272
273    /**
274     * Get the current instance of {@link BluetoothOppService}
275     *
276     * @return current instance of {@link BluetoothOppService}
277     */
278    @VisibleForTesting
279    public static synchronized BluetoothOppService getBluetoothOppService() {
280        if (sBluetoothOppService == null) {
281            Log.w(TAG, "getBluetoothOppService(): service is null");
282            return null;
283        }
284        if (!sBluetoothOppService.isAvailable()) {
285            Log.w(TAG, "getBluetoothOppService(): service is not available");
286            return null;
287        }
288        return sBluetoothOppService;
289    }
290
291    private static synchronized void setBluetoothOppService(BluetoothOppService instance) {
292        if (D) {
293            Log.d(TAG, "setBluetoothOppService(): set to: " + instance);
294        }
295        sBluetoothOppService = instance;
296    }
297
298    private static final int START_LISTENER = 1;
299
300    private static final int MEDIA_SCANNED = 2;
301
302    private static final int MEDIA_SCANNED_FAILED = 3;
303
304    private static final int MSG_INCOMING_CONNECTION_RETRY = 4;
305
306    private static final int MSG_INCOMING_BTOPP_CONNECTION = 100;
307
308    private static final int STOP_LISTENER = 200;
309
310    private Handler mHandler = new Handler() {
311        @Override
312        public void handleMessage(Message msg) {
313            switch (msg.what) {
314                case STOP_LISTENER:
315                    stopListeners();
316                    mListenStarted = false;
317                    //Stop Active INBOUND Transfer
318                    if (mServerTransfer != null) {
319                        mServerTransfer.onBatchCanceled();
320                        mServerTransfer = null;
321                    }
322                    //Stop Active OUTBOUND Transfer
323                    if (mTransfer != null) {
324                        mTransfer.onBatchCanceled();
325                        mTransfer = null;
326                    }
327                    unregisterReceivers();
328                    synchronized (BluetoothOppService.this) {
329                        if (mUpdateThread != null) {
330                            try {
331                                mUpdateThread.interrupt();
332                                mUpdateThread.join();
333                            } catch (InterruptedException e) {
334                                Log.e(TAG, "Interrupted", e);
335                            }
336                            mUpdateThread = null;
337                        }
338                    }
339                    mNotifier.cancelNotifications();
340                    break;
341                case START_LISTENER:
342                    if (mAdapter.isEnabled()) {
343                        startSocketListener();
344                    }
345                    break;
346                case MEDIA_SCANNED:
347                    if (V) {
348                        Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
349                                + msg.obj.toString());
350                    }
351                    ContentValues updateValues = new ContentValues();
352                    Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
353                    updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
354                    updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
355                    updateValues.put(BluetoothShare.MIMETYPE,
356                            getContentResolver().getType(Uri.parse(msg.obj.toString())));
357                    getContentResolver().update(contentUri, updateValues, null, null);
358                    synchronized (BluetoothOppService.this) {
359                        mMediaScanInProgress = false;
360                    }
361                    break;
362                case MEDIA_SCANNED_FAILED:
363                    Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
364                    ContentValues updateValues1 = new ContentValues();
365                    Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
366                    updateValues1.put(Constants.MEDIA_SCANNED,
367                            Constants.MEDIA_SCANNED_SCANNED_FAILED);
368                    getContentResolver().update(contentUri1, updateValues1, null, null);
369                    synchronized (BluetoothOppService.this) {
370                        mMediaScanInProgress = false;
371                    }
372                    break;
373                case MSG_INCOMING_BTOPP_CONNECTION:
374                    if (D) {
375                        Log.d(TAG, "Get incoming connection");
376                    }
377                    ObexTransport transport = (ObexTransport) msg.obj;
378
379                    /*
380                     * Strategy for incoming connections:
381                     * 1. If there is no ongoing transfer, no on-hold connection, start it
382                     * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
383                     * 3. If there is on-hold connection, reject directly
384                     */
385                    if (mBatches.size() == 0 && mPendingConnection == null) {
386                        Log.i(TAG, "Start Obex Server");
387                        createServerSession(transport);
388                    } else {
389                        if (mPendingConnection != null) {
390                            Log.w(TAG, "OPP busy! Reject connection");
391                            try {
392                                transport.close();
393                            } catch (IOException e) {
394                                Log.e(TAG, "close tranport error");
395                            }
396                        } else {
397                            Log.i(TAG, "OPP busy! Retry after 1 second");
398                            mIncomingRetries = mIncomingRetries + 1;
399                            mPendingConnection = transport;
400                            Message msg1 = Message.obtain(mHandler);
401                            msg1.what = MSG_INCOMING_CONNECTION_RETRY;
402                            mHandler.sendMessageDelayed(msg1, 1000);
403                        }
404                    }
405                    break;
406                case MSG_INCOMING_CONNECTION_RETRY:
407                    if (mBatches.size() == 0) {
408                        Log.i(TAG, "Start Obex Server");
409                        createServerSession(mPendingConnection);
410                        mIncomingRetries = 0;
411                        mPendingConnection = null;
412                    } else {
413                        if (mIncomingRetries == 20) {
414                            Log.w(TAG, "Retried 20 seconds, reject connection");
415                            try {
416                                mPendingConnection.close();
417                            } catch (IOException e) {
418                                Log.e(TAG, "close tranport error");
419                            }
420                            if (mServerSocket != null) {
421                                acceptNewConnections();
422                            }
423                            mIncomingRetries = 0;
424                            mPendingConnection = null;
425                        } else {
426                            Log.i(TAG, "OPP busy! Retry after 1 second");
427                            mIncomingRetries = mIncomingRetries + 1;
428                            Message msg2 = Message.obtain(mHandler);
429                            msg2.what = MSG_INCOMING_CONNECTION_RETRY;
430                            mHandler.sendMessageDelayed(msg2, 1000);
431                        }
432                    }
433                    break;
434            }
435        }
436    };
437
438    private ObexServerSockets mServerSocket;
439
440    private void startSocketListener() {
441        if (D) {
442            Log.d(TAG, "start Socket Listeners");
443        }
444        stopListeners();
445        mServerSocket = ObexServerSockets.createInsecure(this);
446        acceptNewConnections();
447        SdpManager sdpManager = SdpManager.getDefaultManager();
448        if (sdpManager == null || mServerSocket == null) {
449            Log.e(TAG, "ERROR:serversocket object is NULL  sdp manager :" + sdpManager
450                    + " mServerSocket:" + mServerSocket);
451            return;
452        }
453        mOppSdpHandle =
454                sdpManager.createOppOpsRecord("OBEX Object Push", mServerSocket.getRfcommChannel(),
455                        mServerSocket.getL2capPsm(), 0x0102, SUPPORTED_OPP_FORMAT);
456        if (D) {
457            Log.d(TAG, "mOppSdpHandle :" + mOppSdpHandle);
458        }
459    }
460
461    @Override
462    protected void cleanup() {
463        if (V) {
464            Log.v(TAG, "onDestroy");
465        }
466        stopListeners();
467        if (mBatches != null) {
468            mBatches.clear();
469        }
470        if (mShares != null) {
471            mShares.clear();
472        }
473        if (mHandler != null) {
474            mHandler.removeCallbacksAndMessages(null);
475        }
476    }
477
478    private void unregisterReceivers() {
479        try {
480            if (mObserver != null) {
481                getContentResolver().unregisterContentObserver(mObserver);
482                mObserver = null;
483            }
484            unregisterReceiver(mBluetoothReceiver);
485        } catch (IllegalArgumentException e) {
486            Log.w(TAG, "unregisterReceivers " + e.toString());
487        }
488    }
489
490    /* suppose we auto accept an incoming OPUSH connection */
491    private void createServerSession(ObexTransport transport) {
492        mServerSession = new BluetoothOppObexServerSession(this, transport, this);
493        mServerSession.preStart();
494        if (D) {
495            Log.d(TAG, "Get ServerSession " + mServerSession.toString() + " for incoming connection"
496                    + transport.toString());
497        }
498    }
499
500    private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
501        @Override
502        public void onReceive(Context context, Intent intent) {
503            String action = intent.getAction();
504
505            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
506                switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
507                    case BluetoothAdapter.STATE_ON:
508                        if (V) {
509                            Log.v(TAG, "Bluetooth state changed: STATE_ON");
510                        }
511                        startListener();
512                        // If this is within a sending process, continue the handle
513                        // logic to display device picker dialog.
514                        synchronized (this) {
515                            if (BluetoothOppManager.getInstance(context).mSendingFlag) {
516                                // reset the flags
517                                BluetoothOppManager.getInstance(context).mSendingFlag = false;
518
519                                Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
520                                in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
521                                in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
522                                        BluetoothDevicePicker.FILTER_TYPE_TRANSFER);
523                                in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE,
524                                        Constants.THIS_PACKAGE_NAME);
525                                in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS,
526                                        BluetoothOppReceiver.class.getName());
527
528                                in1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
529                                context.startActivity(in1);
530                            }
531                        }
532
533                        break;
534                    case BluetoothAdapter.STATE_TURNING_OFF:
535                        if (V) {
536                            Log.v(TAG, "Bluetooth state changed: STATE_TURNING_OFF");
537                        }
538                        mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
539                        break;
540                }
541            }
542        }
543    };
544
545    private void updateFromProvider() {
546        synchronized (BluetoothOppService.this) {
547            mPendingUpdate = true;
548            if (mUpdateThread == null) {
549                mUpdateThread = new UpdateThread();
550                mUpdateThread.start();
551            }
552        }
553    }
554
555    private class UpdateThread extends Thread {
556        private boolean mIsInterrupted;
557
558        UpdateThread() {
559            super("Bluetooth Share Service");
560            mIsInterrupted = false;
561        }
562
563        @Override
564        public void interrupt() {
565            mIsInterrupted = true;
566            if (D) {
567                Log.d(TAG, "OPP UpdateThread interrupted ");
568            }
569            super.interrupt();
570        }
571
572
573        @Override
574        public void run() {
575            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
576
577            while (!mIsInterrupted) {
578                synchronized (BluetoothOppService.this) {
579                    if (mUpdateThread != this) {
580                        throw new IllegalStateException(
581                                "multiple UpdateThreads in BluetoothOppService");
582                    }
583                    if (V) {
584                        Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " sListenStarted is "
585                                + mListenStarted + " isInterrupted :" + mIsInterrupted);
586                    }
587                    if (!mPendingUpdate) {
588                        mUpdateThread = null;
589                        return;
590                    }
591                    mPendingUpdate = false;
592                }
593                Cursor cursor =
594                        getContentResolver().query(BluetoothShare.CONTENT_URI, null, null, null,
595                                BluetoothShare._ID);
596
597                if (cursor == null) {
598                    return;
599                }
600
601                cursor.moveToFirst();
602
603                int arrayPos = 0;
604
605                boolean isAfterLast = cursor.isAfterLast();
606
607                int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
608                /*
609                 * Walk the cursor and the local array to keep them in sync. The
610                 * key to the algorithm is that the ids are unique and sorted
611                 * both in the cursor and in the array, so that they can be
612                 * processed in order in both sources at the same time: at each
613                 * step, both sources point to the lowest id that hasn't been
614                 * processed from that source, and the algorithm processes the
615                 * lowest id from those two possibilities. At each step: -If the
616                 * array contains an entry that's not in the cursor, remove the
617                 * entry, move to next entry in the array. -If the array
618                 * contains an entry that's in the cursor, nothing to do, move
619                 * to next cursor row and next array entry. -If the cursor
620                 * contains an entry that's not in the array, insert a new entry
621                 * in the array, move to next cursor row and next array entry.
622                 */
623                while (!isAfterLast || arrayPos < mShares.size() && mListenStarted) {
624                    if (isAfterLast) {
625                        // We're beyond the end of the cursor but there's still some
626                        // stuff in the local array, which can only be junk
627                        if (mShares.size() != 0) {
628                            if (V) {
629                                Log.v(TAG, "Array update: trimming " + mShares.get(arrayPos).mId
630                                        + " @ " + arrayPos);
631                            }
632                        }
633
634                        if (shouldScanFile(arrayPos)) {
635                            scanFile(arrayPos);
636                        }
637                        deleteShare(arrayPos); // this advances in the array
638                    } else {
639                        int id = cursor.getInt(idColumn);
640
641                        if (arrayPos == mShares.size()) {
642                            insertShare(cursor, arrayPos);
643                            if (V) {
644                                Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
645                            }
646                            ++arrayPos;
647                            cursor.moveToNext();
648                            isAfterLast = cursor.isAfterLast();
649                        } else {
650                            int arrayId = 0;
651                            if (mShares.size() != 0) {
652                                arrayId = mShares.get(arrayPos).mId;
653                            }
654
655                            if (arrayId < id) {
656                                if (V) {
657                                    Log.v(TAG,
658                                            "Array update: removing " + arrayId + " @ " + arrayPos);
659                                }
660                                if (shouldScanFile(arrayPos)) {
661                                    scanFile(arrayPos);
662                                }
663                                deleteShare(arrayPos);
664                            } else if (arrayId == id) {
665                                // This cursor row already exists in the stored array.
666                                updateShare(cursor, arrayPos);
667
668                                ++arrayPos;
669                                cursor.moveToNext();
670                                isAfterLast = cursor.isAfterLast();
671                            } else {
672                                // This cursor entry didn't exist in the stored
673                                // array
674                                if (V) {
675                                    Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
676                                }
677                                insertShare(cursor, arrayPos);
678
679                                ++arrayPos;
680                                cursor.moveToNext();
681                                isAfterLast = cursor.isAfterLast();
682                            }
683                        }
684                    }
685                }
686
687                mNotifier.updateNotification();
688
689                cursor.close();
690            }
691        }
692    }
693
694    private void insertShare(Cursor cursor, int arrayPos) {
695        String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
696        Uri uri;
697        if (uriString != null) {
698            uri = Uri.parse(uriString);
699            Log.d(TAG, "insertShare parsed URI: " + uri);
700        } else {
701            uri = null;
702            Log.e(TAG, "insertShare found null URI at cursor!");
703        }
704        BluetoothOppShareInfo info = new BluetoothOppShareInfo(
705                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)), uri,
706                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
707                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
708                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
709                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
710                cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
711                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
712                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
713                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
714                cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
715                cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
716                cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
717                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED))
718                        != Constants.MEDIA_SCANNED_NOT_SCANNED);
719
720        if (V) {
721            Log.v(TAG, "Service adding new entry");
722            Log.v(TAG, "ID      : " + info.mId);
723            // Log.v(TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
724            Log.v(TAG, "URI     : " + info.mUri);
725            Log.v(TAG, "HINT    : " + info.mHint);
726            Log.v(TAG, "FILENAME: " + info.mFilename);
727            Log.v(TAG, "MIMETYPE: " + info.mMimetype);
728            Log.v(TAG, "DIRECTION: " + info.mDirection);
729            Log.v(TAG, "DESTINAT: " + info.mDestination);
730            Log.v(TAG, "VISIBILI: " + info.mVisibility);
731            Log.v(TAG, "CONFIRM : " + info.mConfirm);
732            Log.v(TAG, "STATUS  : " + info.mStatus);
733            Log.v(TAG, "TOTAL   : " + info.mTotalBytes);
734            Log.v(TAG, "CURRENT : " + info.mCurrentBytes);
735            Log.v(TAG, "TIMESTAMP : " + info.mTimestamp);
736            Log.v(TAG, "SCANNED : " + info.mMediaScanned);
737        }
738
739        mShares.add(arrayPos, info);
740
741        /* Mark the info as failed if it's in invalid status */
742        if (info.isObsolete()) {
743            Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
744        }
745        /*
746         * Add info into a batch. The logic is
747         * 1) Only add valid and readyToStart info
748         * 2) If there is no batch, create a batch and insert this transfer into batch,
749         * then run the batch
750         * 3) If there is existing batch and timestamp match, insert transfer into batch
751         * 4) If there is existing batch and timestamp does not match, create a new batch and
752         * put in queue
753         */
754
755        if (info.isReadyToStart()) {
756            if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
757                /* check if the file exists */
758                BluetoothOppSendFileInfo sendFileInfo =
759                        BluetoothOppUtility.getSendFileInfo(info.mUri);
760                if (sendFileInfo == null || sendFileInfo.mInputStream == null) {
761                    Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
762                    Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
763                    BluetoothOppUtility.closeSendFileInfo(info.mUri);
764                    return;
765                }
766            }
767            if (mBatches.size() == 0) {
768                BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
769                newBatch.mId = mBatchId;
770                mBatchId++;
771                mBatches.add(newBatch);
772                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
773                    if (V) {
774                        Log.v(TAG,
775                                "Service create new Batch " + newBatch.mId + " for OUTBOUND info "
776                                        + info.mId);
777                    }
778                    mTransfer = new BluetoothOppTransfer(this, newBatch);
779                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
780                    if (V) {
781                        Log.v(TAG, "Service create new Batch " + newBatch.mId + " for INBOUND info "
782                                + info.mId);
783                    }
784                    mServerTransfer = new BluetoothOppTransfer(this, newBatch, mServerSession);
785                }
786
787                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
788                    if (V) {
789                        Log.v(TAG, "Service start transfer new Batch " + newBatch.mId + " for info "
790                                + info.mId);
791                    }
792                    mTransfer.start();
793                } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
794                        && mServerTransfer != null) {
795                    if (V) {
796                        Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
797                                + " for info " + info.mId);
798                    }
799                    mServerTransfer.start();
800                }
801
802            } else {
803                int i = findBatchWithTimeStamp(info.mTimestamp);
804                if (i != -1) {
805                    if (V) {
806                        Log.v(TAG, "Service add info " + info.mId + " to existing batch " + mBatches
807                                .get(i).mId);
808                    }
809                    mBatches.get(i).addShare(info);
810                } else {
811                    // There is ongoing batch
812                    BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
813                    newBatch.mId = mBatchId;
814                    mBatchId++;
815                    mBatches.add(newBatch);
816                    if (V) {
817                        Log.v(TAG,
818                                "Service add new Batch " + newBatch.mId + " for info " + info.mId);
819                    }
820                }
821            }
822        }
823    }
824
825    private void updateShare(Cursor cursor, int arrayPos) {
826        BluetoothOppShareInfo info = mShares.get(arrayPos);
827        int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
828
829        info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
830        if (info.mUri != null) {
831            info.mUri =
832                    Uri.parse(stringFromCursor(info.mUri.toString(), cursor, BluetoothShare.URI));
833        } else {
834            Log.w(TAG, "updateShare() called for ID " + info.mId + " with null URI");
835        }
836        info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
837        info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
838        info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
839        info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
840        info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
841        int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
842
843        boolean confirmUpdated = false;
844        int newConfirm =
845                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
846
847        if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
848                && newVisibility != BluetoothShare.VISIBILITY_VISIBLE && (
849                BluetoothShare.isStatusCompleted(info.mStatus)
850                        || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
851            mNotifier.mNotificationMgr.cancel(info.mId);
852        }
853
854        info.mVisibility = newVisibility;
855
856        if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
857                && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
858            confirmUpdated = true;
859        }
860        info.mConfirm =
861                cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
862        int newStatus = cursor.getInt(statusColumn);
863
864        if (BluetoothShare.isStatusCompleted(info.mStatus)) {
865            mNotifier.mNotificationMgr.cancel(info.mId);
866        }
867
868        info.mStatus = newStatus;
869        info.mTotalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
870        info.mCurrentBytes =
871                cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
872        info.mTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
873        info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED))
874                != Constants.MEDIA_SCANNED_NOT_SCANNED);
875
876        if (confirmUpdated) {
877            if (V) {
878                Log.v(TAG, "Service handle info " + info.mId + " confirmation updated");
879            }
880            /* Inbounds transfer user confirmation status changed, update the session server */
881            int i = findBatchWithTimeStamp(info.mTimestamp);
882            if (i != -1) {
883                BluetoothOppBatch batch = mBatches.get(i);
884                if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) {
885                    mServerTransfer.confirmStatusChanged();
886                } //TODO need to think about else
887            }
888        }
889        int i = findBatchWithTimeStamp(info.mTimestamp);
890        if (i != -1) {
891            BluetoothOppBatch batch = mBatches.get(i);
892            if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
893                    || batch.mStatus == Constants.BATCH_STATUS_FAILED) {
894                if (V) {
895                    Log.v(TAG, "Batch " + batch.mId + " is finished");
896                }
897                if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
898                    if (mTransfer == null) {
899                        Log.e(TAG, "Unexpected error! mTransfer is null");
900                    } else if (batch.mId == mTransfer.getBatchId()) {
901                        mTransfer.stop();
902                    } else {
903                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
904                                + " doesn't match mTransfer id " + mTransfer.getBatchId());
905                    }
906                    mTransfer = null;
907                } else {
908                    if (mServerTransfer == null) {
909                        Log.e(TAG, "Unexpected error! mServerTransfer is null");
910                    } else if (batch.mId == mServerTransfer.getBatchId()) {
911                        mServerTransfer.stop();
912                    } else {
913                        Log.e(TAG, "Unexpected error! batch id " + batch.mId
914                                + " doesn't match mServerTransfer id "
915                                + mServerTransfer.getBatchId());
916                    }
917                    mServerTransfer = null;
918                }
919                removeBatch(batch);
920            }
921        }
922    }
923
924    /**
925     * Removes the local copy of the info about a share.
926     */
927    private void deleteShare(int arrayPos) {
928        BluetoothOppShareInfo info = mShares.get(arrayPos);
929
930        /*
931         * Delete arrayPos from a batch. The logic is
932         * 1) Search existing batch for the info
933         * 2) cancel the batch
934         * 3) If the batch become empty delete the batch
935         */
936        int i = findBatchWithTimeStamp(info.mTimestamp);
937        if (i != -1) {
938            BluetoothOppBatch batch = mBatches.get(i);
939            if (batch.hasShare(info)) {
940                if (V) {
941                    Log.v(TAG, "Service cancel batch for share " + info.mId);
942                }
943                batch.cancelBatch();
944            }
945            if (batch.isEmpty()) {
946                if (V) {
947                    Log.v(TAG, "Service remove batch  " + batch.mId);
948                }
949                removeBatch(batch);
950            }
951        }
952        mShares.remove(arrayPos);
953    }
954
955    private String stringFromCursor(String old, Cursor cursor, String column) {
956        int index = cursor.getColumnIndexOrThrow(column);
957        if (old == null) {
958            return cursor.getString(index);
959        }
960        if (mNewChars == null) {
961            mNewChars = new CharArrayBuffer(128);
962        }
963        cursor.copyStringToBuffer(index, mNewChars);
964        int length = mNewChars.sizeCopied;
965        if (length != old.length()) {
966            return cursor.getString(index);
967        }
968        if (mOldChars == null || mOldChars.sizeCopied < length) {
969            mOldChars = new CharArrayBuffer(length);
970        }
971        char[] oldArray = mOldChars.data;
972        char[] newArray = mNewChars.data;
973        old.getChars(0, length, oldArray, 0);
974        for (int i = length - 1; i >= 0; --i) {
975            if (oldArray[i] != newArray[i]) {
976                return new String(newArray, 0, length);
977            }
978        }
979        return old;
980    }
981
982    private int findBatchWithTimeStamp(long timestamp) {
983        for (int i = mBatches.size() - 1; i >= 0; i--) {
984            if (mBatches.get(i).mTimestamp == timestamp) {
985                return i;
986            }
987        }
988        return -1;
989    }
990
991    private void removeBatch(BluetoothOppBatch batch) {
992        if (V) {
993            Log.v(TAG, "Remove batch " + batch.mId);
994        }
995        mBatches.remove(batch);
996        if (mBatches.size() > 0) {
997            for (BluetoothOppBatch nextBatch : mBatches) {
998                // we have a running batch
999                if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) {
1000                    return;
1001                } else {
1002                    // just finish a transfer, start pending outbound transfer
1003                    if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
1004                        if (V) {
1005                            Log.v(TAG, "Start pending outbound batch " + nextBatch.mId);
1006                        }
1007                        mTransfer = new BluetoothOppTransfer(this, nextBatch);
1008                        mTransfer.start();
1009                        return;
1010                    } else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND
1011                            && mServerSession != null) {
1012                        // have to support pending inbound transfer
1013                        // if an outbound transfer and incoming socket happens together
1014                        if (V) {
1015                            Log.v(TAG, "Start pending inbound batch " + nextBatch.mId);
1016                        }
1017                        mServerTransfer = new BluetoothOppTransfer(this, nextBatch, mServerSession);
1018                        mServerTransfer.start();
1019                        if (nextBatch.getPendingShare() != null
1020                                && nextBatch.getPendingShare().mConfirm
1021                                == BluetoothShare.USER_CONFIRMATION_CONFIRMED) {
1022                            mServerTransfer.confirmStatusChanged();
1023                        }
1024                        return;
1025                    }
1026                }
1027            }
1028        }
1029    }
1030
1031    private boolean scanFile(int arrayPos) {
1032        BluetoothOppShareInfo info = mShares.get(arrayPos);
1033        synchronized (BluetoothOppService.this) {
1034            if (D) {
1035                Log.d(TAG, "Scanning file " + info.mFilename);
1036            }
1037            if (!mMediaScanInProgress) {
1038                mMediaScanInProgress = true;
1039                new MediaScannerNotifier(this, info, mHandler);
1040                return true;
1041            } else {
1042                return false;
1043            }
1044        }
1045    }
1046
1047    private boolean shouldScanFile(int arrayPos) {
1048        BluetoothOppShareInfo info = mShares.get(arrayPos);
1049        return BluetoothShare.isStatusSuccess(info.mStatus)
1050                && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned
1051                && info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
1052    }
1053
1054    // Run in a background thread at boot.
1055    private static void trimDatabase(ContentResolver contentResolver) {
1056        // remove the invisible/unconfirmed inbound shares
1057        int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI, WHERE_INVISIBLE_UNCONFIRMED,
1058                null);
1059        if (V) {
1060            Log.v(TAG, "Deleted shares, number = " + delNum);
1061        }
1062
1063        // Keep the latest inbound and successful shares.
1064        Cursor cursor =
1065                contentResolver.query(BluetoothShare.CONTENT_URI, new String[]{BluetoothShare._ID},
1066                        WHERE_INBOUND_SUCCESS, null, BluetoothShare._ID); // sort by id
1067        if (cursor == null) {
1068            return;
1069        }
1070        int recordNum = cursor.getCount();
1071        if (recordNum > Constants.MAX_RECORDS_IN_DATABASE) {
1072            int numToDelete = recordNum - Constants.MAX_RECORDS_IN_DATABASE;
1073
1074            if (cursor.moveToPosition(numToDelete)) {
1075                int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
1076                long id = cursor.getLong(columnId);
1077                delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
1078                        BluetoothShare._ID + " < " + id, null);
1079                if (V) {
1080                    Log.v(TAG, "Deleted old inbound success share: " + delNum);
1081                }
1082            }
1083        }
1084        cursor.close();
1085    }
1086
1087    private static class MediaScannerNotifier implements MediaScannerConnectionClient {
1088
1089        private MediaScannerConnection mConnection;
1090
1091        private BluetoothOppShareInfo mInfo;
1092
1093        private Context mContext;
1094
1095        private Handler mCallback;
1096
1097        MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
1098            mContext = context;
1099            mInfo = info;
1100            mCallback = handler;
1101            mConnection = new MediaScannerConnection(mContext, this);
1102            if (V) {
1103                Log.v(TAG, "Connecting to MediaScannerConnection ");
1104            }
1105            mConnection.connect();
1106        }
1107
1108        @Override
1109        public void onMediaScannerConnected() {
1110            if (V) {
1111                Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
1112            }
1113            mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
1114        }
1115
1116        @Override
1117        public void onScanCompleted(String path, Uri uri) {
1118            try {
1119                if (V) {
1120                    Log.v(TAG, "MediaScannerConnection onScanCompleted");
1121                    Log.v(TAG, "MediaScannerConnection path is " + path);
1122                    Log.v(TAG, "MediaScannerConnection Uri is " + uri);
1123                }
1124                if (uri != null) {
1125                    Message msg = Message.obtain();
1126                    msg.setTarget(mCallback);
1127                    msg.what = MEDIA_SCANNED;
1128                    msg.arg1 = mInfo.mId;
1129                    msg.obj = uri;
1130                    msg.sendToTarget();
1131                } else {
1132                    Message msg = Message.obtain();
1133                    msg.setTarget(mCallback);
1134                    msg.what = MEDIA_SCANNED_FAILED;
1135                    msg.arg1 = mInfo.mId;
1136                    msg.sendToTarget();
1137                }
1138            } catch (NullPointerException ex) {
1139                Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
1140            } finally {
1141                if (V) {
1142                    Log.v(TAG, "MediaScannerConnection disconnect");
1143                }
1144                mConnection.disconnect();
1145            }
1146        }
1147    }
1148
1149    private void stopListeners() {
1150        if (mAdapter != null && mOppSdpHandle >= 0 && SdpManager.getDefaultManager() != null) {
1151            if (D) {
1152                Log.d(TAG, "Removing SDP record mOppSdpHandle :" + mOppSdpHandle);
1153            }
1154            boolean status = SdpManager.getDefaultManager().removeSdpRecord(mOppSdpHandle);
1155            Log.d(TAG, "RemoveSDPrecord returns " + status);
1156            mOppSdpHandle = -1;
1157        }
1158        if (mServerSocket != null) {
1159            mServerSocket.shutdown(false);
1160            mServerSocket = null;
1161        }
1162        if (D) {
1163            Log.d(TAG, "stopListeners: mServerSocket is null");
1164        }
1165    }
1166
1167    @Override
1168    public boolean onConnect(BluetoothDevice device, BluetoothSocket socket) {
1169
1170        if (D) {
1171            Log.d(TAG, " onConnect BluetoothSocket :" + socket + " \n :device :" + device);
1172        }
1173        if (!mAcceptNewConnections) {
1174            Log.d(TAG, " onConnect BluetoothSocket :" + socket + " rejected");
1175            return false;
1176        }
1177        BluetoothObexTransport transport = new BluetoothObexTransport(socket);
1178        Message msg = mHandler.obtainMessage(MSG_INCOMING_BTOPP_CONNECTION);
1179        msg.obj = transport;
1180        msg.sendToTarget();
1181        mAcceptNewConnections = false;
1182        return true;
1183    }
1184
1185    @Override
1186    public void onAcceptFailed() {
1187        Log.d(TAG, " onAcceptFailed:");
1188        mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER));
1189    }
1190
1191    /**
1192     * Set mAcceptNewConnections to true to allow new connections.
1193     */
1194    void acceptNewConnections() {
1195        mAcceptNewConnections = true;
1196    }
1197}
1198