| /* |
| * Copyright (c) 2008-2009, Motorola, Inc. |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * |
| * - Neither the name of the Motorola, Inc. nor the names of its contributors |
| * may be used to endorse or promote products derived from this software |
| * without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| * POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package com.android.bluetooth.opp; |
| |
| import android.app.NotificationManager; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothSocket; |
| import android.bluetooth.BluetoothUuid; |
| import android.bluetooth.SdpOppOpsRecord; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.ParcelUuid; |
| import android.os.Process; |
| import android.util.Log; |
| |
| import com.android.bluetooth.BluetoothObexTransport; |
| |
| import java.io.File; |
| import java.io.IOException; |
| |
| import javax.obex.ObexTransport; |
| |
| /** |
| * This class run an actual Opp transfer session (from connect target device to |
| * disconnect) |
| */ |
| public class BluetoothOppTransfer implements BluetoothOppBatch.BluetoothOppBatchListener { |
| private static final String TAG = "BtOppTransfer"; |
| |
| private static final boolean D = Constants.DEBUG; |
| |
| private static final boolean V = Constants.VERBOSE; |
| |
| private static final int TRANSPORT_ERROR = 10; |
| |
| private static final int TRANSPORT_CONNECTED = 11; |
| |
| private static final int SOCKET_ERROR_RETRY = 13; |
| |
| private static final int CONNECT_WAIT_TIMEOUT = 45000; |
| |
| private static final int CONNECT_RETRY_TIME = 100; |
| |
| private static final String SOCKET_LINK_KEY_ERROR = "Invalid exchange"; |
| |
| private Context mContext; |
| |
| private BluetoothAdapter mAdapter; |
| |
| private BluetoothDevice mDevice; |
| |
| private BluetoothOppBatch mBatch; |
| |
| private BluetoothOppObexSession mSession; |
| |
| private BluetoothOppShareInfo mCurrentShare; |
| |
| private ObexTransport mTransport; |
| |
| private HandlerThread mHandlerThread; |
| |
| private EventHandler mSessionHandler; |
| |
| private long mTimestamp; |
| |
| private class OppConnectionReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (D) { |
| Log.d(TAG, " Action :" + action); |
| } |
| if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (device == null || mBatch == null || mCurrentShare == null) { |
| Log.e(TAG, "device : " + device + " mBatch :" + mBatch + " mCurrentShare :" |
| + mCurrentShare); |
| return; |
| } |
| try { |
| if (V) { |
| Log.v(TAG, "Device :" + device + "- OPP device: " + mBatch.mDestination |
| + " \n mCurrentShare.mConfirm == " + mCurrentShare.mConfirm); |
| } |
| if ((device.equals(mBatch.mDestination)) && (mCurrentShare.mConfirm |
| == BluetoothShare.USER_CONFIRMATION_PENDING)) { |
| if (V) { |
| Log.v(TAG, "ACTION_ACL_DISCONNECTED to be processed for batch: " |
| + mBatch.mId); |
| } |
| // Remove the timeout message triggered earlier during Obex Put |
| mSessionHandler.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT); |
| // Now reuse the same message to clean up the session. |
| mSessionHandler.sendMessage(mSessionHandler.obtainMessage( |
| BluetoothOppObexSession.MSG_CONNECT_TIMEOUT)); |
| } |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { |
| ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); |
| if (D) { |
| Log.d(TAG, "Received UUID: " + uuid.toString()); |
| Log.d(TAG, "expected UUID: " + BluetoothUuid.ObexObjectPush.toString()); |
| } |
| if (uuid.equals(BluetoothUuid.ObexObjectPush)) { |
| int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); |
| Log.d(TAG, " -> status: " + status); |
| BluetoothDevice device = |
| intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (mDevice == null) { |
| Log.w(TAG, "OPP SDP search, target device is null, ignoring result"); |
| return; |
| } |
| if (!device.getAddress().equalsIgnoreCase(mDevice.getAddress())) { |
| Log.w(TAG, " OPP SDP search for wrong device, ignoring!!"); |
| return; |
| } |
| SdpOppOpsRecord record = |
| intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); |
| if (record == null) { |
| Log.w(TAG, " Invalid SDP , ignoring !!"); |
| markConnectionFailed(null); |
| return; |
| } |
| mConnectThread = |
| new SocketConnectThread(mDevice, false, true, record.getL2capPsm()); |
| mConnectThread.start(); |
| mDevice = null; |
| } |
| } |
| } |
| } |
| |
| private OppConnectionReceiver mBluetoothReceiver; |
| |
| public BluetoothOppTransfer(Context context, BluetoothOppBatch batch, |
| BluetoothOppObexSession session) { |
| |
| mContext = context; |
| mBatch = batch; |
| mSession = session; |
| |
| mBatch.registerListern(this); |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| |
| } |
| |
| public BluetoothOppTransfer(Context context, BluetoothOppBatch batch) { |
| this(context, batch, null); |
| } |
| |
| public int getBatchId() { |
| return mBatch.mId; |
| } |
| |
| /* |
| * Receives events from mConnectThread & mSession back in the main thread. |
| */ |
| private class EventHandler extends Handler { |
| EventHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case SOCKET_ERROR_RETRY: |
| mConnectThread = new SocketConnectThread((BluetoothDevice) msg.obj, true); |
| |
| mConnectThread.start(); |
| break; |
| case TRANSPORT_ERROR: |
| /* |
| * RFCOMM connect fail is for outbound share only! Mark batch |
| * failed, and all shares in batch failed |
| */ |
| if (V) { |
| Log.v(TAG, "receive TRANSPORT_ERROR msg"); |
| } |
| mConnectThread = null; |
| markBatchFailed(BluetoothShare.STATUS_CONNECTION_ERROR); |
| mBatch.mStatus = Constants.BATCH_STATUS_FAILED; |
| |
| break; |
| case TRANSPORT_CONNECTED: |
| /* |
| * RFCOMM connected is for outbound share only! Create |
| * BluetoothOppObexClientSession and start it |
| */ |
| if (V) { |
| Log.v(TAG, "Transfer receive TRANSPORT_CONNECTED msg"); |
| } |
| mConnectThread = null; |
| mTransport = (ObexTransport) msg.obj; |
| startObexSession(); |
| |
| break; |
| case BluetoothOppObexSession.MSG_SHARE_COMPLETE: |
| /* |
| * Put next share if available,or finish the transfer. |
| * For outbound session, call session.addShare() to send next file, |
| * or call session.stop(). |
| * For inbounds session, do nothing. If there is next file to receive,it |
| * will be notified through onShareAdded() |
| */ |
| BluetoothOppShareInfo info = (BluetoothOppShareInfo) msg.obj; |
| if (V) { |
| Log.v(TAG, "receive MSG_SHARE_COMPLETE for info " + info.mId); |
| } |
| if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| mCurrentShare = mBatch.getPendingShare(); |
| |
| if (mCurrentShare != null) { |
| /* we have additional share to process */ |
| if (V) { |
| Log.v(TAG, "continue session for info " + mCurrentShare.mId |
| + " from batch " + mBatch.mId); |
| } |
| processCurrentShare(); |
| } else { |
| /* for outbound transfer, all shares are processed */ |
| if (V) { |
| Log.v(TAG, "Batch " + mBatch.mId + " is done"); |
| } |
| mSession.stop(); |
| } |
| } |
| break; |
| case BluetoothOppObexSession.MSG_SESSION_COMPLETE: |
| /* |
| * Handle session completed status Set batch status to |
| * finished |
| */ |
| cleanUp(); |
| BluetoothOppShareInfo info1 = (BluetoothOppShareInfo) msg.obj; |
| if (V) { |
| Log.v(TAG, "receive MSG_SESSION_COMPLETE for batch " + mBatch.mId); |
| } |
| mBatch.mStatus = Constants.BATCH_STATUS_FINISHED; |
| /* |
| * trigger content provider again to know batch status change |
| */ |
| tickShareStatus(info1); |
| break; |
| |
| case BluetoothOppObexSession.MSG_SESSION_ERROR: |
| /* Handle the error state of an Obex session */ |
| if (V) { |
| Log.v(TAG, "receive MSG_SESSION_ERROR for batch " + mBatch.mId); |
| } |
| cleanUp(); |
| try { |
| BluetoothOppShareInfo info2 = (BluetoothOppShareInfo) msg.obj; |
| if (mSession != null) { |
| mSession.stop(); |
| } |
| mBatch.mStatus = Constants.BATCH_STATUS_FAILED; |
| markBatchFailed(info2.mStatus); |
| tickShareStatus(mCurrentShare); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception while handling MSG_SESSION_ERROR"); |
| e.printStackTrace(); |
| } |
| break; |
| |
| case BluetoothOppObexSession.MSG_SHARE_INTERRUPTED: |
| if (V) { |
| Log.v(TAG, "receive MSG_SHARE_INTERRUPTED for batch " + mBatch.mId); |
| } |
| BluetoothOppShareInfo info3 = (BluetoothOppShareInfo) msg.obj; |
| if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| try { |
| if (mTransport == null) { |
| Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null"); |
| } else { |
| mTransport.close(); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "failed to close mTransport"); |
| } |
| if (V) { |
| Log.v(TAG, "mTransport closed "); |
| } |
| mBatch.mStatus = Constants.BATCH_STATUS_FAILED; |
| if (info3 != null) { |
| markBatchFailed(info3.mStatus); |
| } else { |
| markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR); |
| } |
| tickShareStatus(mCurrentShare); |
| } |
| break; |
| |
| case BluetoothOppObexSession.MSG_CONNECT_TIMEOUT: |
| if (V) { |
| Log.v(TAG, "receive MSG_CONNECT_TIMEOUT for batch " + mBatch.mId); |
| } |
| /* for outbound transfer, the block point is BluetoothSocket.write() |
| * The only way to unblock is to tear down lower transport |
| * */ |
| if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| try { |
| if (mTransport == null) { |
| Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null"); |
| } else { |
| mTransport.close(); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "failed to close mTransport"); |
| } |
| if (V) { |
| Log.v(TAG, "mTransport closed "); |
| } |
| } else { |
| /* |
| * For inbound transfer, the block point is waiting for |
| * user confirmation we can interrupt it nicely |
| */ |
| |
| // Remove incoming file confirm notification |
| NotificationManager nm = (NotificationManager) mContext.getSystemService( |
| Context.NOTIFICATION_SERVICE); |
| nm.cancel(mCurrentShare.mId); |
| // Send intent to UI for timeout handling |
| Intent in = new Intent(BluetoothShare.USER_CONFIRMATION_TIMEOUT_ACTION); |
| mContext.sendBroadcast(in); |
| |
| markShareTimeout(mCurrentShare); |
| } |
| break; |
| } |
| } |
| } |
| |
| private void markShareTimeout(BluetoothOppShareInfo share) { |
| Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId); |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(BluetoothShare.USER_CONFIRMATION, |
| BluetoothShare.USER_CONFIRMATION_TIMEOUT); |
| mContext.getContentResolver().update(contentUri, updateValues, null, null); |
| } |
| |
| private void markBatchFailed(int failReason) { |
| synchronized (this) { |
| try { |
| wait(1000); |
| } catch (InterruptedException e) { |
| if (V) { |
| Log.v(TAG, "Interrupted waiting for markBatchFailed"); |
| } |
| } |
| } |
| |
| if (D) { |
| Log.d(TAG, "Mark all ShareInfo in the batch as failed"); |
| } |
| if (mCurrentShare != null) { |
| if (V) { |
| Log.v(TAG, "Current share has status " + mCurrentShare.mStatus); |
| } |
| if (BluetoothShare.isStatusError(mCurrentShare.mStatus)) { |
| failReason = mCurrentShare.mStatus; |
| } |
| if (mCurrentShare.mDirection == BluetoothShare.DIRECTION_INBOUND |
| && mCurrentShare.mFilename != null) { |
| new File(mCurrentShare.mFilename).delete(); |
| } |
| } |
| |
| BluetoothOppShareInfo info = null; |
| if (mBatch == null) { |
| return; |
| } |
| info = mBatch.getPendingShare(); |
| while (info != null) { |
| if (info.mStatus < 200) { |
| info.mStatus = failReason; |
| Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + info.mId); |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(BluetoothShare.STATUS, info.mStatus); |
| /* Update un-processed outbound transfer to show some info */ |
| if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| BluetoothOppSendFileInfo fileInfo = |
| BluetoothOppUtility.getSendFileInfo(info.mUri); |
| BluetoothOppUtility.closeSendFileInfo(info.mUri); |
| if (fileInfo.mFileName != null) { |
| updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName); |
| updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength); |
| updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype); |
| } |
| } else { |
| if (info.mStatus < 200 && info.mFilename != null) { |
| new File(info.mFilename).delete(); |
| } |
| } |
| mContext.getContentResolver().update(contentUri, updateValues, null, null); |
| Constants.sendIntentIfCompleted(mContext, contentUri, info.mStatus); |
| } |
| info = mBatch.getPendingShare(); |
| } |
| |
| } |
| |
| /* |
| * NOTE |
| * For outbound transfer |
| * 1) Check Bluetooth status |
| * 2) Start handler thread |
| * 3) new a thread to connect to target device |
| * 3.1) Try a few times to do SDP query for target device OPUSH channel |
| * 3.2) Try a few seconds to connect to target socket |
| * 4) After BluetoothSocket is connected,create an instance of RfcommTransport |
| * 5) Create an instance of BluetoothOppClientSession |
| * 6) Start the session and process the first share in batch |
| * For inbound transfer |
| * The transfer already has session and transport setup, just start it |
| * 1) Check Bluetooth status |
| * 2) Start handler thread |
| * 3) Start the session and process the first share in batch |
| */ |
| |
| /** |
| * Start the transfer |
| */ |
| public void start() { |
| /* check Bluetooth enable status */ |
| /* |
| * normally it's impossible to reach here if BT is disabled. Just check |
| * for safety |
| */ |
| if (!mAdapter.isEnabled()) { |
| Log.e(TAG, "Can't start transfer when Bluetooth is disabled for " + mBatch.mId); |
| markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR); |
| mBatch.mStatus = Constants.BATCH_STATUS_FAILED; |
| return; |
| } |
| |
| if (mHandlerThread == null) { |
| if (V) { |
| Log.v(TAG, "Create handler thread for batch " + mBatch.mId); |
| } |
| mHandlerThread = |
| new HandlerThread("BtOpp Transfer Handler", Process.THREAD_PRIORITY_BACKGROUND); |
| mHandlerThread.start(); |
| mSessionHandler = new EventHandler(mHandlerThread.getLooper()); |
| |
| if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| /* for outbound transfer, we do connect first */ |
| startConnectSession(); |
| } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) { |
| /* |
| * for inbound transfer, it's already connected, so we start |
| * OBEX session directly |
| */ |
| startObexSession(); |
| } |
| } |
| registerConnectionreceiver(); |
| } |
| |
| /** |
| * Stop the transfer |
| */ |
| public void stop() { |
| if (V) { |
| Log.v(TAG, "stop"); |
| } |
| if (mSession != null) { |
| if (V) { |
| Log.v(TAG, "Stop mSession"); |
| } |
| mSession.stop(); |
| } |
| |
| cleanUp(); |
| if (mConnectThread != null) { |
| try { |
| mConnectThread.interrupt(); |
| if (V) { |
| Log.v(TAG, "waiting for connect thread to terminate"); |
| } |
| mConnectThread.join(); |
| } catch (InterruptedException e) { |
| if (V) { |
| Log.v(TAG, "Interrupted waiting for connect thread to join"); |
| } |
| } |
| mConnectThread = null; |
| } |
| // Prevent concurrent access |
| synchronized (this) { |
| if (mHandlerThread != null) { |
| mHandlerThread.quit(); |
| mHandlerThread.interrupt(); |
| mHandlerThread = null; |
| } |
| } |
| } |
| |
| private void startObexSession() { |
| |
| mBatch.mStatus = Constants.BATCH_STATUS_RUNNING; |
| |
| mCurrentShare = mBatch.getPendingShare(); |
| if (mCurrentShare == null) { |
| /* |
| * TODO catch this error |
| */ |
| Log.e(TAG, "Unexpected error happened !"); |
| return; |
| } |
| if (V) { |
| Log.v(TAG, "Start session for info " + mCurrentShare.mId + " for batch " + mBatch.mId); |
| } |
| |
| if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) { |
| if (V) { |
| Log.v(TAG, "Create Client session with transport " + mTransport.toString()); |
| } |
| mSession = new BluetoothOppObexClientSession(mContext, mTransport); |
| } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) { |
| /* |
| * For inbounds transfer, a server session should already exists |
| * before BluetoothOppTransfer is initialized. We should pass in a |
| * mSession instance. |
| */ |
| if (mSession == null) { |
| /** set current share as error */ |
| Log.e(TAG, "Unexpected error happened !"); |
| markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR); |
| mBatch.mStatus = Constants.BATCH_STATUS_FAILED; |
| return; |
| } |
| if (V) { |
| Log.v(TAG, "Transfer has Server session" + mSession.toString()); |
| } |
| } |
| |
| mSession.start(mSessionHandler, mBatch.getNumShares()); |
| processCurrentShare(); |
| } |
| |
| private void registerConnectionreceiver() { |
| /* |
| * OBEX channel need to be monitored for unexpected ACL disconnection |
| * such as Remote Battery removal |
| */ |
| synchronized (this) { |
| try { |
| if (mBluetoothReceiver == null) { |
| mBluetoothReceiver = new OppConnectionReceiver(); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); |
| filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); |
| mContext.registerReceiver(mBluetoothReceiver, filter); |
| if (V) { |
| Log.v(TAG, "Registered mBluetoothReceiver"); |
| } |
| } |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "mBluetoothReceiver Registered already ", e); |
| } |
| } |
| } |
| |
| private void processCurrentShare() { |
| /* This transfer need user confirm */ |
| if (V) { |
| Log.v(TAG, "processCurrentShare" + mCurrentShare.mId); |
| } |
| mSession.addShare(mCurrentShare); |
| if (mCurrentShare.mConfirm == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED) { |
| confirmStatusChanged(); |
| } |
| } |
| |
| /** |
| * Set transfer confirmed status. It should only be called for inbound |
| * transfer |
| */ |
| public void confirmStatusChanged() { |
| /* unblock server session */ |
| final Thread notifyThread = new Thread("Server Unblock thread") { |
| @Override |
| public void run() { |
| synchronized (mSession) { |
| mSession.unblock(); |
| mSession.notify(); |
| } |
| } |
| }; |
| if (V) { |
| Log.v(TAG, "confirmStatusChanged to unblock mSession" + mSession.toString()); |
| } |
| notifyThread.start(); |
| } |
| |
| private void startConnectSession() { |
| mDevice = mBatch.mDestination; |
| if (!mBatch.mDestination.sdpSearch(BluetoothUuid.ObexObjectPush)) { |
| if (D) { |
| Log.d(TAG, "SDP failed, start rfcomm connect directly"); |
| } |
| /* update bd address as sdp could not be started */ |
| mDevice = null; |
| /* SDP failed, start rfcomm connect directly */ |
| mConnectThread = new SocketConnectThread(mBatch.mDestination, false, false, -1); |
| mConnectThread.start(); |
| } |
| } |
| |
| private SocketConnectThread mConnectThread; |
| |
| private class SocketConnectThread extends Thread { |
| private final String mHost; |
| |
| private final BluetoothDevice mDevice; |
| |
| private final int mChannel; |
| |
| private int mL2cChannel = 0; |
| |
| private boolean mIsConnected; |
| |
| private long mTimestamp; |
| |
| private BluetoothSocket mBtSocket = null; |
| |
| private boolean mRetry = false; |
| |
| private boolean mSdpInitiated = false; |
| |
| private boolean mIsInterrupted = false; |
| |
| /* create a Rfcomm/L2CAP Socket */ |
| SocketConnectThread(BluetoothDevice device, boolean retry) { |
| super("Socket Connect Thread"); |
| this.mDevice = device; |
| this.mHost = null; |
| this.mChannel = -1; |
| mIsConnected = false; |
| mRetry = retry; |
| mSdpInitiated = false; |
| } |
| |
| /* create a Rfcomm/L2CAP Socket */ |
| SocketConnectThread(BluetoothDevice device, boolean retry, boolean sdpInitiated, |
| int l2capChannel) { |
| super("Socket Connect Thread"); |
| this.mDevice = device; |
| this.mHost = null; |
| this.mChannel = -1; |
| mIsConnected = false; |
| mRetry = retry; |
| mSdpInitiated = sdpInitiated; |
| mL2cChannel = l2capChannel; |
| } |
| |
| @Override |
| public void interrupt() { |
| if (D) { |
| Log.d(TAG, "start interrupt :" + mBtSocket); |
| } |
| mIsInterrupted = true; |
| if (mBtSocket != null) { |
| try { |
| mBtSocket.close(); |
| } catch (IOException e) { |
| Log.v(TAG, "Error when close socket"); |
| } |
| } |
| } |
| |
| private void connectRfcommSocket() { |
| if (V) { |
| Log.v(TAG, "connectRfcommSocket"); |
| } |
| try { |
| if (mIsInterrupted) { |
| Log.d(TAG, "connectRfcommSocket interrupted"); |
| markConnectionFailed(mBtSocket); |
| return; |
| } |
| mBtSocket = mDevice.createInsecureRfcommSocketToServiceRecord( |
| BluetoothUuid.ObexObjectPush.getUuid()); |
| } catch (IOException e1) { |
| Log.e(TAG, "Rfcomm socket create error", e1); |
| markConnectionFailed(mBtSocket); |
| return; |
| } |
| try { |
| mBtSocket.connect(); |
| |
| if (V) { |
| Log.v(TAG, |
| "Rfcomm socket connection attempt took " + (System.currentTimeMillis() |
| - mTimestamp) + " ms"); |
| } |
| BluetoothObexTransport transport; |
| transport = new BluetoothObexTransport(mBtSocket); |
| |
| BluetoothOppPreference.getInstance(mContext).setName(mDevice, mDevice.getName()); |
| |
| if (V) { |
| Log.v(TAG, "Send transport message " + transport.toString()); |
| } |
| |
| mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget(); |
| } catch (IOException e) { |
| Log.e(TAG, "Rfcomm socket connect exception", e); |
| // If the devices were paired before, but unpaired on the |
| // remote end, it will return an error for the auth request |
| // for the socket connection. Link keys will get exchanged |
| // again, but we need to retry. There is no good way to |
| // inform this socket asking it to retry apart from a blind |
| // delayed retry. |
| if (!mRetry && e.getMessage().equals(SOCKET_LINK_KEY_ERROR)) { |
| Message msg = |
| mSessionHandler.obtainMessage(SOCKET_ERROR_RETRY, -1, -1, mDevice); |
| mSessionHandler.sendMessageDelayed(msg, 1500); |
| } else { |
| markConnectionFailed(mBtSocket); |
| } |
| } |
| } |
| |
| @Override |
| public void run() { |
| mTimestamp = System.currentTimeMillis(); |
| if (D) { |
| Log.d(TAG, "sdp initiated = " + mSdpInitiated + " l2cChannel :" + mL2cChannel); |
| } |
| // check if sdp initiated successfully for l2cap or not. If not |
| // connect |
| // directly to rfcomm |
| if (!mSdpInitiated || mL2cChannel < 0) { |
| /* sdp failed for some reason, connect on rfcomm */ |
| Log.d(TAG, "sdp not initiated, connecting on rfcomm"); |
| connectRfcommSocket(); |
| return; |
| } |
| |
| /* Reset the flag */ |
| mSdpInitiated = false; |
| |
| /* Use BluetoothSocket to connect */ |
| try { |
| if (mIsInterrupted) { |
| Log.e(TAG, "btSocket connect interrupted "); |
| markConnectionFailed(mBtSocket); |
| return; |
| } else { |
| mBtSocket = mDevice.createInsecureL2capSocket(mL2cChannel); |
| } |
| } catch (IOException e1) { |
| Log.e(TAG, "L2cap socket create error", e1); |
| connectRfcommSocket(); |
| return; |
| } |
| try { |
| mBtSocket.connect(); |
| if (V) { |
| Log.v(TAG, "L2cap socket connection attempt took " + (System.currentTimeMillis() |
| - mTimestamp) + " ms"); |
| } |
| BluetoothObexTransport transport; |
| transport = new BluetoothObexTransport(mBtSocket); |
| BluetoothOppPreference.getInstance(mContext).setName(mDevice, mDevice.getName()); |
| if (V) { |
| Log.v(TAG, "Send transport message " + transport.toString()); |
| } |
| mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget(); |
| } catch (IOException e) { |
| Log.e(TAG, "L2cap socket connect exception", e); |
| try { |
| mBtSocket.close(); |
| } catch (IOException e3) { |
| Log.e(TAG, "Bluetooth socket close error ", e3); |
| } |
| connectRfcommSocket(); |
| return; |
| } |
| } |
| } |
| |
| private void markConnectionFailed(BluetoothSocket s) { |
| if (V) { |
| Log.v(TAG, "markConnectionFailed " + s); |
| } |
| try { |
| if (s != null) { |
| s.close(); |
| } |
| } catch (IOException e) { |
| if (V) { |
| Log.e(TAG, "Error when close socket"); |
| } |
| } |
| mSessionHandler.obtainMessage(TRANSPORT_ERROR).sendToTarget(); |
| return; |
| } |
| |
| /* update a trivial field of a share to notify Provider the batch status change */ |
| private void tickShareStatus(BluetoothOppShareInfo share) { |
| if (share == null) { |
| Log.d(TAG, "Share is null"); |
| return; |
| } |
| Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId); |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(BluetoothShare.DIRECTION, share.mDirection); |
| mContext.getContentResolver().update(contentUri, updateValues, null, null); |
| } |
| |
| /* |
| * Note: For outbound transfer We don't implement this method now. If later |
| * we want to support merging a later added share into an existing session, |
| * we could implement here For inbounds transfer add share means it's |
| * multiple receive in the same session, we should handle it to fill it into |
| * mSession |
| */ |
| |
| /** |
| * Process when a share is added to current transfer |
| */ |
| @Override |
| public void onShareAdded(int id) { |
| BluetoothOppShareInfo info = mBatch.getPendingShare(); |
| if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) { |
| mCurrentShare = mBatch.getPendingShare(); |
| /* |
| * TODO what if it's not auto confirmed? |
| */ |
| if (mCurrentShare != null && ( |
| mCurrentShare.mConfirm == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED |
| || mCurrentShare.mConfirm |
| == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED)) { |
| /* have additional auto confirmed share to process */ |
| if (V) { |
| Log.v(TAG, "Transfer continue session for info " + mCurrentShare.mId |
| + " from batch " + mBatch.mId); |
| } |
| processCurrentShare(); |
| confirmStatusChanged(); |
| } |
| } |
| } |
| |
| /* |
| * NOTE We don't implement this method now. Now delete a single share from |
| * the batch means the whole batch should be canceled. If later we want to |
| * support single cancel, we could implement here For outbound transfer, if |
| * the share is currently in transfer, cancel it For inbounds transfer, |
| * delete share means the current receiving file should be canceled. |
| */ |
| |
| /** |
| * Process when a share is deleted from current transfer |
| */ |
| @Override |
| public void onShareDeleted(int id) { |
| |
| } |
| |
| /** |
| * Process when current transfer is canceled |
| */ |
| @Override |
| public void onBatchCanceled() { |
| if (V) { |
| Log.v(TAG, "Transfer on Batch canceled"); |
| } |
| |
| this.stop(); |
| mBatch.mStatus = Constants.BATCH_STATUS_FINISHED; |
| } |
| |
| private void cleanUp() { |
| synchronized (this) { |
| try { |
| if (mBluetoothReceiver != null) { |
| mContext.unregisterReceiver(mBluetoothReceiver); |
| mBluetoothReceiver = null; |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Exception:unregisterReceiver"); |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |