BluetoothOppTransfer.java revision 3a88b20fcd71e42451e402d27374b19eeb2ff0da
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 javax.obex.ObexTransport;
36
37import android.app.NotificationManager;
38import android.bluetooth.BluetoothAdapter;
39import android.bluetooth.BluetoothDevice;
40import android.bluetooth.BluetoothSocket;
41import android.bluetooth.BluetoothUuid;
42import android.os.ParcelUuid;
43import android.content.BroadcastReceiver;
44import android.content.ContentValues;
45import android.content.Context;
46import android.content.Intent;
47import android.content.IntentFilter;
48import android.net.Uri;
49import android.os.Handler;
50import android.os.HandlerThread;
51import android.os.Looper;
52import android.os.Message;
53import android.os.Parcelable;
54import android.os.PowerManager;
55import android.os.Process;
56import android.util.Log;
57
58import java.io.File;
59import java.io.IOException;
60import java.net.InetSocketAddress;
61import java.net.Socket;
62import java.net.UnknownHostException;
63
64/**
65 * This class run an actual Opp transfer session (from connect target device to
66 * disconnect)
67 */
68public class BluetoothOppTransfer implements BluetoothOppBatch.BluetoothOppBatchListener {
69    private static final String TAG = "BtOppTransfer";
70
71    private static final boolean D = Constants.DEBUG;
72
73    private static final boolean V = Constants.VERBOSE;
74
75    public static final int RFCOMM_ERROR = 10;
76
77    public static final int RFCOMM_CONNECTED = 11;
78
79    public static final int SDP_RESULT = 12;
80
81    private static final int CONNECT_WAIT_TIMEOUT = 45000;
82
83    private static final int CONNECT_RETRY_TIME = 100;
84
85    private static final short OPUSH_UUID16 = 0x1105;
86
87    private Context mContext;
88
89    private BluetoothAdapter mAdapter;
90
91    private BluetoothOppBatch mBatch;
92
93    private BluetoothOppObexSession mSession;
94
95    private BluetoothOppShareInfo mCurrentShare;
96
97    private ObexTransport mTransport;
98
99    private HandlerThread mHandlerThread;
100
101    private EventHandler mSessionHandler;
102
103    /*
104     * TODO check if we need PowerManager here
105     */
106    private PowerManager mPowerManager;
107
108    private long mTimestamp;
109
110    public BluetoothOppTransfer(Context context, PowerManager powerManager,
111            BluetoothOppBatch batch, BluetoothOppObexSession session) {
112
113        mContext = context;
114        mPowerManager = powerManager;
115        mBatch = batch;
116        mSession = session;
117
118        mBatch.registerListern(this);
119        mAdapter = BluetoothAdapter.getDefaultAdapter();
120
121    }
122
123    public BluetoothOppTransfer(Context context, PowerManager powerManager, BluetoothOppBatch batch) {
124        this(context, powerManager, batch, null);
125    }
126
127    public int getBatchId() {
128        return mBatch.mId;
129    }
130
131    /*
132     * Receives events from mConnectThread & mSession back in the main thread.
133     */
134    private class EventHandler extends Handler {
135        public EventHandler(Looper looper) {
136            super(looper);
137        }
138
139        @Override
140        public void handleMessage(Message msg) {
141            switch (msg.what) {
142                case SDP_RESULT:
143                    if (V) Log.v(TAG, "SDP request returned " + msg.arg1 + " (" +
144                            (System.currentTimeMillis() - mTimestamp + " ms)"));
145                    if (!((BluetoothDevice)msg.obj).equals(mBatch.mDestination)) {
146                        return;
147                    }
148                    try {
149                        mContext.unregisterReceiver(mReceiver);
150                    } catch (IllegalArgumentException e) {
151                        // ignore
152                    }
153                    if (msg.arg1 > 0) {
154                        mConnectThread = new SocketConnectThread(mBatch.mDestination, msg.arg1);
155                        mConnectThread.start();
156                    } else {
157                        /* SDP query fail case */
158                        Log.e(TAG, "SDP query failed!");
159                        markBatchFailed(BluetoothShare.STATUS_CONNECTION_ERROR);
160                        mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
161                    }
162
163                    break;
164
165                /*
166                 * RFCOMM connect fail is for outbound share only! Mark batch
167                 * failed, and all shares in batch failed
168                 */
169                case RFCOMM_ERROR:
170                    if (V) Log.v(TAG, "receive RFCOMM_ERROR msg");
171                    mConnectThread = null;
172                    markBatchFailed(BluetoothShare.STATUS_CONNECTION_ERROR);
173                    mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
174
175                    break;
176                /*
177                 * RFCOMM connected is for outbound share only! Create
178                 * BluetoothOppObexClientSession and start it
179                 */
180                case RFCOMM_CONNECTED:
181                    if (V) Log.v(TAG, "Transfer receive RFCOMM_CONNECTED msg");
182                    mConnectThread = null;
183                    mTransport = (ObexTransport)msg.obj;
184                    startObexSession();
185
186                    break;
187                /*
188                 * Put next share if available,or finish the transfer.
189                 * For outbound session, call session.addShare() to send next file,
190                 * or call session.stop().
191                 * For inbounds session, do nothing. If there is next file to receive,it
192                 * will be notified through onShareAdded()
193                 */
194                case BluetoothOppObexSession.MSG_SHARE_COMPLETE:
195                    BluetoothOppShareInfo info = (BluetoothOppShareInfo)msg.obj;
196                    if (V) Log.v(TAG, "receive MSG_SHARE_COMPLETE for info " + info.mId);
197                    if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
198                        mCurrentShare = mBatch.getPendingShare();
199
200                        if (mCurrentShare != null) {
201                            /* we have additional share to process */
202                            if (V) Log.v(TAG, "continue session for info " + mCurrentShare.mId +
203                                    " from batch " + mBatch.mId);
204                            processCurrentShare();
205                        } else {
206                            /* for outbound transfer, all shares are processed */
207                            if (V) Log.v(TAG, "Batch " + mBatch.mId + " is done");
208                            mSession.stop();
209                        }
210                    }
211                    break;
212                /*
213                 * Handle session completed status Set batch status to
214                 * finished
215                 */
216                case BluetoothOppObexSession.MSG_SESSION_COMPLETE:
217                    BluetoothOppShareInfo info1 = (BluetoothOppShareInfo)msg.obj;
218                    if (V) Log.v(TAG, "receive MSG_SESSION_COMPLETE for batch " + mBatch.mId);
219                    mBatch.mStatus = Constants.BATCH_STATUS_FINISHED;
220                    /*
221                     * trigger content provider again to know batch status change
222                     */
223                    tickShareStatus(info1);
224                    break;
225
226                /* Handle the error state of an Obex session */
227                case BluetoothOppObexSession.MSG_SESSION_ERROR:
228                    if (V) Log.v(TAG, "receive MSG_SESSION_ERROR for batch " + mBatch.mId);
229                    BluetoothOppShareInfo info2 = (BluetoothOppShareInfo)msg.obj;
230                    mSession.stop();
231                    mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
232                    markBatchFailed(info2.mStatus);
233                    tickShareStatus(mCurrentShare);
234                    break;
235
236                case BluetoothOppObexSession.MSG_SHARE_INTERRUPTED:
237                    if (V) Log.v(TAG, "receive MSG_SHARE_INTERRUPTED for batch " + mBatch.mId);
238                    BluetoothOppShareInfo info3 = (BluetoothOppShareInfo)msg.obj;
239                    if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
240                        try {
241                            if (mTransport == null) {
242                                Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null");
243                            } else {
244                                mTransport.close();
245                            }
246                        } catch (IOException e) {
247                            Log.e(TAG, "failed to close mTransport");
248                        }
249                        if (V) Log.v(TAG, "mTransport closed ");
250                        mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
251                        if (info3 != null) {
252                            markBatchFailed(info3.mStatus);
253                        } else {
254                            markBatchFailed();
255                        }
256                        tickShareStatus(mCurrentShare);
257                    }
258                    break;
259
260                case BluetoothOppObexSession.MSG_CONNECT_TIMEOUT:
261                    if (V) Log.v(TAG, "receive MSG_CONNECT_TIMEOUT for batch " + mBatch.mId);
262                    /* for outbound transfer, the block point is BluetoothSocket.write()
263                     * The only way to unblock is to tear down lower transport
264                     * */
265                    if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
266                        try {
267                            if (mTransport == null) {
268                                Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null");
269                            } else {
270                                mTransport.close();
271                            }
272                        } catch (IOException e) {
273                            Log.e(TAG, "failed to close mTransport");
274                        }
275                        if (V) Log.v(TAG, "mTransport closed ");
276                    } else {
277                        /*
278                         * For inbound transfer, the block point is waiting for
279                         * user confirmation we can interrupt it nicely
280                         */
281
282                        // Remove incoming file confirm notification
283                        NotificationManager nm = (NotificationManager)mContext
284                                .getSystemService(Context.NOTIFICATION_SERVICE);
285                        nm.cancel(mCurrentShare.mId);
286                        // Send intent to UI for timeout handling
287                        Intent in = new Intent(BluetoothShare.USER_CONFIRMATION_TIMEOUT_ACTION);
288                        mContext.sendBroadcast(in);
289
290                        markShareTimeout(mCurrentShare);
291                    }
292                    break;
293            }
294        }
295    }
296
297    private void markShareTimeout(BluetoothOppShareInfo share) {
298        Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId);
299        ContentValues updateValues = new ContentValues();
300        updateValues
301                .put(BluetoothShare.USER_CONFIRMATION, BluetoothShare.USER_CONFIRMATION_TIMEOUT);
302        mContext.getContentResolver().update(contentUri, updateValues, null, null);
303    }
304
305    private void markBatchFailed(int failReason) {
306        synchronized (this) {
307            try {
308                wait(1000);
309            } catch (InterruptedException e) {
310                if (V) Log.v(TAG, "Interrupted waiting for markBatchFailed");
311            }
312        }
313
314        if (D) Log.d(TAG, "Mark all ShareInfo in the batch as failed");
315        if (mCurrentShare != null) {
316            if (V) Log.v(TAG, "Current share has status " + mCurrentShare.mStatus);
317            if (BluetoothShare.isStatusError(mCurrentShare.mStatus)) {
318                failReason = mCurrentShare.mStatus;
319            }
320            if (mCurrentShare.mDirection == BluetoothShare.DIRECTION_INBOUND
321                    && mCurrentShare.mFilename != null) {
322                new File(mCurrentShare.mFilename).delete();
323            }
324        }
325
326        BluetoothOppShareInfo info = mBatch.getPendingShare();
327        while (info != null) {
328            if (info.mStatus < 200) {
329                info.mStatus = failReason;
330                Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + info.mId);
331                ContentValues updateValues = new ContentValues();
332                updateValues.put(BluetoothShare.STATUS, info.mStatus);
333                /* Update un-processed outbound transfer to show some info */
334                if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
335                    BluetoothOppSendFileInfo fileInfo = null;
336                    fileInfo = BluetoothOppSendFileInfo.generateFileInfo(mContext, info.mUri,
337                            info.mMimetype, info.mDestination);
338                    if (fileInfo.mFileName != null) {
339                        updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
340                        updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
341                        updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype);
342                    }
343                } else {
344                    if (info.mStatus < 200 && info.mFilename != null) {
345                        new File(info.mFilename).delete();
346                    }
347                }
348                mContext.getContentResolver().update(contentUri, updateValues, null, null);
349                Constants.sendIntentIfCompleted(mContext, contentUri, info.mStatus);
350            }
351            info = mBatch.getPendingShare();
352        }
353
354    }
355
356    private void markBatchFailed() {
357        markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR);
358    }
359
360    /*
361     * NOTE
362     * For outbound transfer
363     * 1) Check Bluetooth status
364     * 2) Start handler thread
365     * 3) new a thread to connect to target device
366     * 3.1) Try a few times to do SDP query for target device OPUSH channel
367     * 3.2) Try a few seconds to connect to target socket
368     * 4) After BluetoothSocket is connected,create an instance of RfcommTransport
369     * 5) Create an instance of BluetoothOppClientSession
370     * 6) Start the session and process the first share in batch
371     * For inbound transfer
372     * The transfer already has session and transport setup, just start it
373     * 1) Check Bluetooth status
374     * 2) Start handler thread
375     * 3) Start the session and process the first share in batch
376     */
377    /**
378     * Start the transfer
379     */
380    public void start() {
381        /* check Bluetooth enable status */
382        /*
383         * normally it's impossible to reach here if BT is disabled. Just check
384         * for safety
385         */
386        if (!mAdapter.isEnabled()) {
387            Log.e(TAG, "Can't start transfer when Bluetooth is disabled for " + mBatch.mId);
388            markBatchFailed();
389            mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
390            return;
391        }
392
393        if (mHandlerThread == null) {
394            if (V) Log.v(TAG, "Create handler thread for batch " + mBatch.mId);
395            mHandlerThread = new HandlerThread("BtOpp Transfer Handler",
396                    Process.THREAD_PRIORITY_BACKGROUND);
397            mHandlerThread.start();
398            mSessionHandler = new EventHandler(mHandlerThread.getLooper());
399
400            if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
401                /* for outbound transfer, we do connect first */
402                startConnectSession();
403            } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
404                /*
405                 * for inbound transfer, it's already connected, so we start
406                 * OBEX session directly
407                 */
408                startObexSession();
409            }
410        }
411    }
412
413    /**
414     * Stop the transfer
415     */
416    public void stop() {
417        if (V) Log.v(TAG, "stop");
418        if (mConnectThread != null) {
419            try {
420                mConnectThread.interrupt();
421                if (V) Log.v(TAG, "waiting for connect thread to terminate");
422                mConnectThread.join();
423            } catch (InterruptedException e) {
424                if (V) Log.v(TAG, "Interrupted waiting for connect thread to join");
425            }
426            mConnectThread = null;
427        }
428        if (mSession != null) {
429            if (V) Log.v(TAG, "Stop mSession");
430            mSession.stop();
431        }
432        if (mHandlerThread != null) {
433            mHandlerThread.getLooper().quit();
434            mHandlerThread.interrupt();
435            mHandlerThread = null;
436        }
437    }
438
439    private void startObexSession() {
440
441        mBatch.mStatus = Constants.BATCH_STATUS_RUNNING;
442
443        mCurrentShare = mBatch.getPendingShare();
444        if (mCurrentShare == null) {
445            /*
446             * TODO catch this error
447             */
448            Log.e(TAG, "Unexpected error happened !");
449            return;
450        }
451        if (V) Log.v(TAG, "Start session for info " + mCurrentShare.mId + " for batch " +
452                mBatch.mId);
453
454        if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
455            if (V) Log.v(TAG, "Create Client session with transport " + mTransport.toString());
456            mSession = new BluetoothOppObexClientSession(mContext, mTransport);
457        } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
458            /*
459             * For inbounds transfer, a server session should already exists
460             * before BluetoothOppTransfer is initialized. We should pass in a
461             * mSession instance.
462             */
463            if (mSession == null) {
464                /** set current share as error */
465                Log.e(TAG, "Unexpected error happened !");
466                markBatchFailed();
467                mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
468                return;
469            }
470            if (V) Log.v(TAG, "Transfer has Server session" + mSession.toString());
471        }
472
473        mSession.start(mSessionHandler);
474        processCurrentShare();
475    }
476
477    private void processCurrentShare() {
478        /* This transfer need user confirm */
479        if (V) Log.v(TAG, "processCurrentShare" + mCurrentShare.mId);
480        mSession.addShare(mCurrentShare);
481    }
482
483    /**
484     * Set transfer confirmed status. It should only be called for inbound
485     * transfer
486     */
487    public void setConfirmed() {
488        /* unblock server session */
489        final Thread notifyThread = new Thread("Server Unblock thread") {
490            public void run() {
491                synchronized (mSession) {
492                    mSession.unblock();
493                    mSession.notify();
494                }
495            }
496        };
497        if (V) Log.v(TAG, "setConfirmed to unblock mSession" + mSession.toString());
498        notifyThread.start();
499    }
500
501    private void startConnectSession() {
502
503        if (Constants.USE_TCP_DEBUG) {
504            mConnectThread = new SocketConnectThread("localhost", Constants.TCP_DEBUG_PORT, 0);
505            mConnectThread.start();
506        } else {
507            int channel = BluetoothOppPreference.getInstance(mContext).getChannel(
508                    mBatch.mDestination, OPUSH_UUID16);
509            if (channel != -1) {
510                if (D) Log.d(TAG, "Get OPUSH channel " + channel + " from cache for " +
511                        mBatch.mDestination);
512                mTimestamp = System.currentTimeMillis();
513                mSessionHandler.obtainMessage(SDP_RESULT, channel, -1, mBatch.mDestination)
514                        .sendToTarget();
515            } else {
516                doOpushSdp();
517            }
518        }
519    }
520
521    private void doOpushSdp() {
522        if (V) Log.v(TAG, "Do Opush SDP request for address " + mBatch.mDestination);
523
524        mTimestamp = System.currentTimeMillis();
525
526        int channel;
527        channel = mBatch.mDestination.getServiceChannel(BluetoothUuid.ObexObjectPush);
528        if (channel != -1) {
529            if (D) Log.d(TAG, "Get OPUSH channel " + channel + " from SDP for "
530                    + mBatch.mDestination);
531
532            mSessionHandler.obtainMessage(SDP_RESULT, channel, -1, mBatch.mDestination)
533                    .sendToTarget();
534            return;
535
536        } else {
537            if (V) Log.v(TAG, "Remote Service channel not in cache");
538
539            if (!mBatch.mDestination.fetchUuidsWithSdp()) {
540                Log.e(TAG, "Start SDP query failed");
541            } else {
542                // we expect framework send us Intent ACTION_UUID. otherwise we will fail
543                if (V) Log.v(TAG, "Start new SDP, wait for result");
544                IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_UUID);
545                mContext.registerReceiver(mReceiver, intentFilter);
546                return;
547            }
548        }
549        Message msg = mSessionHandler.obtainMessage(SDP_RESULT, channel, -1, mBatch.mDestination);
550        mSessionHandler.sendMessageDelayed(msg, 2000);
551    }
552
553    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
554        @Override
555        public void onReceive(Context context, Intent intent) {
556            if (intent.getAction().equals(BluetoothDevice.ACTION_UUID)) {
557                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
558                if (V) Log.v(TAG, "ACTION_UUID for device " + device);
559                if (device.equals(mBatch.mDestination)) {
560                    int channel = -1;
561                    Parcelable[] uuid = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
562                    if (uuid != null) {
563                        ParcelUuid[] uuids = new ParcelUuid[uuid.length];
564                        for (int i = 0; i < uuid.length; i++) {
565                            uuids[i] = (ParcelUuid)uuid[i];
566                        }
567                        if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.ObexObjectPush)) {
568                            if (V) Log.v(TAG, "SDP get OPP result for device " + device);
569                            channel = mBatch.mDestination
570                                    .getServiceChannel(BluetoothUuid.ObexObjectPush);
571                        }
572                    }
573                    mSessionHandler.obtainMessage(SDP_RESULT, channel, -1, mBatch.mDestination)
574                            .sendToTarget();
575                }
576            }
577        }
578    };
579
580    private SocketConnectThread mConnectThread;
581
582    private class SocketConnectThread extends Thread {
583        private final String host;
584
585        private final BluetoothDevice device;
586
587        private final int channel;
588
589        private boolean isConnected;
590
591        private long timestamp;
592
593        private BluetoothSocket btSocket = null;
594
595        /* create a TCP socket */
596        public SocketConnectThread(String host, int port, int dummy) {
597            super("Socket Connect Thread");
598            this.host = host;
599            this.channel = port;
600            this.device = null;
601            isConnected = false;
602        }
603
604        /* create a Rfcomm Socket */
605        public SocketConnectThread(BluetoothDevice device, int channel) {
606            super("Socket Connect Thread");
607            this.device = device;
608            this.host = null;
609            this.channel = channel;
610            isConnected = false;
611        }
612
613        public void interrupt() {
614            if (!Constants.USE_TCP_DEBUG) {
615                if (btSocket != null) {
616                    try {
617                        btSocket.close();
618                    } catch (IOException e) {
619                        Log.v(TAG, "Error when close socket");
620                    }
621                }
622            }
623        }
624
625        @Override
626        public void run() {
627
628            timestamp = System.currentTimeMillis();
629
630            if (Constants.USE_TCP_DEBUG) {
631                /* Use TCP socket to connect */
632                Socket s = new Socket();
633
634                // Try to connect for 50 seconds
635                int result = 0;
636                for (int i = 0; i < CONNECT_RETRY_TIME && result == 0; i++) {
637                    try {
638                        s.connect(new InetSocketAddress(host, channel), CONNECT_WAIT_TIMEOUT);
639                    } catch (UnknownHostException e) {
640                        Log.e(TAG, "TCP socket connect unknown host ");
641                    } catch (IOException e) {
642                        Log.e(TAG, "TCP socket connect failed ");
643                    }
644                    if (s.isConnected()) {
645                        if (D) Log.d(TAG, "TCP socket connected ");
646                        isConnected = true;
647                        break;
648                    }
649                    if (isInterrupted()) {
650                        Log.e(TAG, "TCP socket connect interrupted ");
651                        markConnectionFailed(s);
652                        return;
653                    }
654                }
655                if (!isConnected) {
656                    Log.e(TAG, "TCP socket connect failed after 20 seconds");
657                    markConnectionFailed(s);
658                    return;
659                }
660
661                if (V) Log.v(TAG, "TCP Socket connection attempt took " +
662                        (System.currentTimeMillis() - timestamp) + " ms");
663
664                TestTcpTransport transport;
665                transport = new TestTcpTransport(s);
666
667                if (isInterrupted()) {
668                    isConnected = false;
669                    markConnectionFailed(s);
670                    transport = null;
671                    return;
672                }
673                if (!isConnected) {
674                    transport = null;
675                    Log.e(TAG, "TCP connect session error: ");
676                    markConnectionFailed(s);
677                    return;
678                } else {
679                    if (D) Log.d(TAG, "Send transport message " + transport.toString());
680                    mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
681                }
682            } else {
683
684                /* Use BluetoothSocket to connect */
685
686                try {
687                    btSocket = device.createInsecureRfcommSocket(channel);
688                } catch (IOException e1) {
689                    Log.e(TAG, "Rfcomm socket create error");
690                    markConnectionFailed(btSocket);
691                    return;
692                }
693                try {
694                    btSocket.connect();
695
696                    if (V) Log.v(TAG, "Rfcomm socket connection attempt took " +
697                            (System.currentTimeMillis() - timestamp) + " ms");
698                    BluetoothOppRfcommTransport transport;
699                    transport = new BluetoothOppRfcommTransport(btSocket);
700
701                    BluetoothOppPreference.getInstance(mContext).setChannel(device, OPUSH_UUID16,
702                            channel);
703                    BluetoothOppPreference.getInstance(mContext).setName(device, device.getName());
704
705                    if (V) Log.v(TAG, "Send transport message " + transport.toString());
706
707                    mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
708                } catch (IOException e) {
709                    Log.e(TAG, "Rfcomm socket connect exception ");
710                    BluetoothOppPreference.getInstance(mContext)
711                            .removeChannel(device, OPUSH_UUID16);
712                    markConnectionFailed(btSocket);
713                    return;
714                }
715            }
716        }
717
718        private void markConnectionFailed(Socket s) {
719            try {
720                s.close();
721            } catch (IOException e) {
722                Log.e(TAG, "TCP socket close error");
723            }
724            mSessionHandler.obtainMessage(RFCOMM_ERROR).sendToTarget();
725        }
726
727        private void markConnectionFailed(BluetoothSocket s) {
728            try {
729                s.close();
730            } catch (IOException e) {
731                if (V) Log.e(TAG, "Error when close socket");
732            }
733            mSessionHandler.obtainMessage(RFCOMM_ERROR).sendToTarget();
734            return;
735        }
736    };
737
738    /* update a trivial field of a share to notify Provider the batch status change */
739    private void tickShareStatus(BluetoothOppShareInfo share) {
740        Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId);
741        ContentValues updateValues = new ContentValues();
742        updateValues.put(BluetoothShare.DIRECTION, share.mDirection);
743        mContext.getContentResolver().update(contentUri, updateValues, null, null);
744    }
745
746    /*
747     * Note: For outbound transfer We don't implement this method now. If later
748     * we want to support merging a later added share into an existing session,
749     * we could implement here For inbounds transfer add share means it's
750     * multiple receive in the same session, we should handle it to fill it into
751     * mSession
752     */
753    /**
754     * Process when a share is added to current transfer
755     */
756    public void onShareAdded(int id) {
757        BluetoothOppShareInfo info = mBatch.getPendingShare();
758        if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
759            mCurrentShare = mBatch.getPendingShare();
760            /*
761             * TODO what if it's not auto confirmed?
762             */
763            if (mCurrentShare != null
764                    && mCurrentShare.mConfirm == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED) {
765                /* have additional auto confirmed share to process */
766                if (V) Log.v(TAG, "Transfer continue session for info " + mCurrentShare.mId +
767                        " from batch " + mBatch.mId);
768                processCurrentShare();
769                setConfirmed();
770            }
771        }
772    }
773
774    /*
775     * NOTE We don't implement this method now. Now delete a single share from
776     * the batch means the whole batch should be canceled. If later we want to
777     * support single cancel, we could implement here For outbound transfer, if
778     * the share is currently in transfer, cancel it For inbounds transfer,
779     * delete share means the current receiving file should be canceled.
780     */
781    /**
782     * Process when a share is deleted from current transfer
783     */
784    public void onShareDeleted(int id) {
785
786    }
787
788    /**
789     * Process when current transfer is canceled
790     */
791    public void onBatchCanceled() {
792        if (V) Log.v(TAG, "Transfer on Batch canceled");
793
794        this.stop();
795        mBatch.mStatus = Constants.BATCH_STATUS_FINISHED;
796    }
797}
798