/* * Copyright (C) 2015 Samsung System LSI * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth; import java.io.IOException; import java.util.concurrent.CountDownLatch; import javax.obex.ObexSession; import javax.obex.ResponseCodes; import javax.obex.ServerSession; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.util.Log; /** * Wraps multiple BluetoothServerSocket objects to make it possible to accept connections on * both a RFCOMM and L2CAP channel in parallel.
* Create an instance using {@link #create()}, which will block until the sockets have been created * and channel numbers have been assigned.
* Use {@link #getRfcommChannel()} and {@link #getL2capPsm()} to get the channel numbers to * put into the SDP record.
* Call {@link #shutdown(boolean)} to terminate the accept threads created by the call to * {@link #create(IObexConnectionHandler)}.
* A reference to an object of this type cannot be reused, and the {@link BluetoothServerSocket} * object references passed to this object will be closed by this object, hence cannot be reused * either (This is needed, as the only way to interrupt an accept call is to close the socket...) *
* When a connection is accepted, * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.
* If the an error occur while waiting for an incoming connection * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.
* In both cases the {@link ObexServerSockets} object have terminated, and a new must be created. */ public class ObexServerSockets { private final String TAG; private static final String STAG = "ObexServerSockets"; private static final boolean D = true; // TODO: set to false! private static final int NUMBER_OF_SOCKET_TYPES = 2; // increment if LE will be supported private final IObexConnectionHandler mConHandler; /* The wrapped sockets */ private final BluetoothServerSocket mRfcommSocket; private final BluetoothServerSocket mL2capSocket; /* Handles to the accept threads. Needed for shutdown. */ private SocketAcceptThread mRfcommThread = null; private SocketAcceptThread mL2capThread = null; private volatile boolean mConAccepted = false; private static volatile int sInstanceCounter = 0; private ObexServerSockets(IObexConnectionHandler conHandler, BluetoothServerSocket rfcommSocket, BluetoothServerSocket l2capSocket) { mConHandler = conHandler; mRfcommSocket = rfcommSocket; mL2capSocket = l2capSocket; TAG = "ObexServerSockets" + sInstanceCounter++; } /** * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket} * @param validator a reference to the {@link IObexConnectionHandler} object to call * to validate an incoming connection. * @return a reference to a {@link ObexServerSockets} object instance. * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s. */ public static ObexServerSockets create(IObexConnectionHandler validator) { return create(validator, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP); } /** * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket} * @param validator a reference to the {@link IObexConnectionHandler} object to call * to validate an incoming connection. * @param rfcommChannel fixed rfcomm channel number to listen on * @param l2capPsm fixed l2cap psm to listen on * @return a reference to a {@link ObexServerSockets} object instance. * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s. */ public static ObexServerSockets createWithFixedChannels(IObexConnectionHandler validator, int rfcommChannel, int l2capPsm) { return create(validator, rfcommChannel, l2capPsm); } /** * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket} * with specific l2cap and RFCOMM channel numbers. It is the responsibility of the caller to * ensure the numbers are free and can be used, e.g. by calling {@link #getL2capPsm()} and * {@link #getRfcommChannel()} in {@link ObexServerSockets}. * @param validator a reference to the {@link IObexConnectionHandler} object to call * to validate an incoming connection. * @return a reference to a {@link ObexServerSockets} object instance. * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s. * * TODO: Make public when it becomes possible to determine that the listen-call * failed due to channel-in-use. */ private static ObexServerSockets create(IObexConnectionHandler validator, int rfcommChannel, int l2capPsm) { if(D) Log.d(STAG,"create(rfcomm = " +rfcommChannel + ", l2capPsm = " + l2capPsm +")"); BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); if(bt == null) { throw new RuntimeException("No bluetooth adapter..."); } BluetoothServerSocket rfcommSocket = null; BluetoothServerSocket l2capSocket = null; boolean initSocketOK = false; final int CREATE_RETRY_TIME = 10; // It's possible that create will fail in some cases. retry for 10 times for (int i = 0; i < CREATE_RETRY_TIME; i++) { initSocketOK = true; try { if(rfcommSocket == null && rfcommChannel != -1) { rfcommSocket = bt.listenUsingRfcommOn(rfcommChannel); } if(l2capSocket == null && l2capPsm != -1) { l2capSocket = bt.listenUsingL2capOn(l2capPsm); } } catch (IOException e) { Log.e(STAG, "Error create ServerSockets ",e); initSocketOK = false; } if (!initSocketOK) { // Need to break out of this loop if BT is being turned off. int state = bt.getState(); if ((state != BluetoothAdapter.STATE_TURNING_ON) && (state != BluetoothAdapter.STATE_ON)) { Log.w(STAG, "initServerSockets failed as BT is (being) turned off"); break; } try { if (D) Log.v(STAG, "waiting 300 ms..."); Thread.sleep(300); } catch (InterruptedException e) { Log.e(STAG, "create() was interrupted"); } } else { break; } } if (initSocketOK) { if (D) Log.d(STAG, "Succeed to create listening sockets "); ObexServerSockets sockets = new ObexServerSockets(validator, rfcommSocket, l2capSocket); sockets.startAccept(); return sockets; } else { Log.e(STAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try"); return null; } } /** * Returns the channel number assigned to the RFCOMM socket. This will be a static value, that * should be reused for multiple connections. * @return the RFCOMM channel number */ public int getRfcommChannel() { return mRfcommSocket.getChannel(); } /** * Returns the channel number assigned to the L2CAP socket. This will be a static value, that * should be reused for multiple connections. * @return the L2CAP channel number */ public int getL2capPsm() { return mL2capSocket.getChannel(); } /** * Initiate the accept threads. * Will create a thread for each socket type. an incoming connection will be signaled to * the {@link IObexConnectionValidator#onConnect()}, at which point both threads will exit. */ private void startAccept() { if(D) Log.d(TAG,"startAccept()"); prepareForNewConnect(); if (mRfcommSocket != null) { mRfcommThread = new SocketAcceptThread(mRfcommSocket); mRfcommThread.start(); } if (mL2capSocket != null) { mL2capThread = new SocketAcceptThread(mL2capSocket); mL2capThread.start(); } } /** * Set state to accept new incoming connection. Will cause the next incoming connection to be * Signaled through {@link IObexConnectionValidator#onConnect()}; */ public void prepareForNewConnect() { if(D) Log.d(TAG, "prepareForNewConnect()"); mConAccepted = false; } /** * Called from the AcceptThreads to signal an incoming connection. * This is the entry point that needs to synchronize between the accept * threads, and ensure only a single connection is accepted. * {@link mAcceptedSocket} is used a state variable. * @param device the connecting device. * @param conSocket the socket associated with the connection. * @return true if the connection is accepted, false otherwise. */ synchronized private boolean onConnect(BluetoothDevice device, BluetoothSocket conSocket) { if(D) Log.d(TAG, "onConnect() socket: " + conSocket + " mConAccepted = " + mConAccepted); if(mConAccepted == false && mConHandler.onConnect(device, conSocket) == true) { mConAccepted = true; // TODO: Reset this when ready to accept new connection /* Signal the remaining threads to stop. shutdown(false); */ // UPDATE: TODO: remove - redesigned to keep running... return true; } return false; } /** * Signal to the {@link IObexConnectionHandler} that an error have occurred. */ synchronized private void onAcceptFailed() { //Donot block for Accept thread cleanup. //Fix Handler Thread block during BT Turn OFF. shutdown(false); BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); if((mAdapter != null) && (mAdapter.getState() == BluetoothAdapter.STATE_ON)) { Log.d(TAG,"onAcceptFailed() calling shutdown..."); mConHandler.onAcceptFailed(); } } /** * Terminate any running accept threads * @param block Set true to block the calling thread until the AcceptThreads * has ended execution */ synchronized public void shutdown(boolean block) { if(D) Log.d(TAG, "shutdown(block = " + block + ")"); if(mRfcommThread != null) { mRfcommThread.shutdown(); } if(mL2capThread != null){ mL2capThread.shutdown(); } if(block == true) { while(mRfcommThread != null || mL2capThread != null) { try { if(mRfcommThread != null) { mRfcommThread.join(); mRfcommThread = null; } if(mL2capThread != null) { mL2capThread.join(); mL2capThread = null; } } catch (InterruptedException e) { Log.i(TAG, "shutdown() interrupted, continue waiting...", e); } } } else { mRfcommThread = null; mL2capThread = null; } } /** * A thread that runs in the background waiting for remote an incoming * connect. Once a remote socket connects, this thread will be * shutdown. When the remote disconnect, this thread shall be restarted to * accept a new connection. */ private class SocketAcceptThread extends Thread { private boolean mStopped = false; private final BluetoothServerSocket mServerSocket; /** * Create a SocketAcceptThread * @param serverSocket shall never be null. * @param latch shall never be null. * @throws IllegalArgumentException */ public SocketAcceptThread(BluetoothServerSocket serverSocket) { if(serverSocket == null) { throw new IllegalArgumentException("serverSocket cannot be null"); } mServerSocket = serverSocket; } /** * Run until shutdown of BT. * Accept incoming connections and reject if needed. Keep accepting incoming connections. */ @Override public void run() { try { while (!mStopped) { BluetoothSocket connSocket; BluetoothDevice device; try { if (D) Log.d(TAG, "Accepting socket connection..."); connSocket = mServerSocket.accept(); if (D) Log.d(TAG, "Accepted socket connection from: " + mServerSocket); if (connSocket == null) { // TODO: Do we need a max error count, to avoid spinning? Log.w(TAG, "connSocket is null - reattempt accept"); continue; } device = connSocket.getRemoteDevice(); if (device == null) { Log.i(TAG, "getRemoteDevice() = null - reattempt accept"); try{ connSocket.close(); } catch (IOException e) { Log.w(TAG, "Error closing the socket. ignoring...",e ); } continue; } /* Signal to the service that we have received an incoming connection. */ boolean isValid = ObexServerSockets.this.onConnect(device, connSocket); if(isValid == false) { /* Close connection if we already have a connection with another device * by responding to the OBEX connect request. */ Log.i(TAG, "RemoteDevice is invalid - creating ObexRejectServer."); BluetoothObexTransport obexTrans = new BluetoothObexTransport(connSocket); // Create and detach a selfdestructing ServerSession to respond to any // incoming OBEX signals. new ServerSession(obexTrans, new ObexRejectServer( ResponseCodes.OBEX_HTTP_UNAVAILABLE, connSocket), null); // now wait for a new connect } else { // now wait for a new connect } } catch (IOException ex) { if(mStopped == true) { // Expected exception because of shutdown. } else { Log.w(TAG, "Accept exception for " + mServerSocket, ex); ObexServerSockets.this.onAcceptFailed(); } mStopped=true; } } // End while() } finally { if (D) Log.d(TAG, "AcceptThread ended for: " + mServerSocket); } } /** * Shuts down the accept threads, and closes the ServerSockets, causing all related * BluetoothSockets to disconnect, hence do not call until all all accepted connections * are ready to be disconnected. */ public void shutdown() { if(mStopped == false) { mStopped = true; // TODO: According to the documentation, this should not close the accepted // sockets - and that is true, but it closes the l2cap connections, and // therefore it implicitly also closes the accepted sockets... try { mServerSocket.close(); } catch (IOException e) { if(D) Log.d(TAG, "Exception while thread shutdown:", e); } } // If called from another thread, interrupt the thread if(!Thread.currentThread().equals(this)){ // TODO: Will this interrupt the thread if it is blocked in synchronized? // Else: change to use InterruptableLock if(D) Log.d(TAG, "shutdown called from another thread - interrupt()."); interrupt(); } } } }