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