blob: 789f12f45d9614444af9e79c498f59deaa1a745d [file] [log] [blame]
/*
* 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 android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import java.io.IOException;
import javax.obex.ResponseCodes;
import javax.obex.ServerSession;
/**
* Wraps multiple BluetoothServerSocket objects to make it possible to accept connections on
* both a RFCOMM and L2CAP channel in parallel.<br>
* Create an instance using {@link #create()}, which will block until the sockets have been created
* and channel numbers have been assigned.<br>
* Use {@link #getRfcommChannel()} and {@link #getL2capPsm()} to get the channel numbers to
* put into the SDP record.<br>
* Call {@link #shutdown(boolean)} to terminate the accept threads created by the call to
* {@link #create(IObexConnectionHandler)}.<br>
* 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...)
* <br>
* When a connection is accepted,
* {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
* If the an error occur while waiting for an incoming connection
* {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
* In both cases the {@link ObexServerSockets} object have terminated, and a new must be created.
*/
public class ObexServerSockets {
private final String mTag;
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;
private SocketAcceptThread mL2capThread;
private static volatile int sInstanceCounter;
private ObexServerSockets(IObexConnectionHandler conHandler, BluetoothServerSocket rfcommSocket,
BluetoothServerSocket l2capSocket) {
mConHandler = conHandler;
mRfcommSocket = rfcommSocket;
mL2capSocket = l2capSocket;
mTag = "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, true);
}
/**
* Creates an Insecure 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 createInsecure(IObexConnectionHandler validator) {
return create(validator, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP,
BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false);
}
private static final int CREATE_RETRY_TIME = 10;
/**
* 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.
* @param isSecure boolean flag to determine whther socket would be secured or inseucure.
* @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, boolean isSecure) {
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;
// 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) {
if (isSecure) {
rfcommSocket = bt.listenUsingRfcommOn(rfcommChannel);
} else {
rfcommSocket = bt.listenUsingInsecureRfcommOn(rfcommChannel);
}
}
if (l2capSocket == null) {
if (isSecure) {
l2capSocket = bt.listenUsingL2capOn(l2capPsm);
} else {
l2capSocket = bt.listenUsingInsecureL2capOn(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(mTag, "startAccept()");
}
mRfcommThread = new SocketAcceptThread(mRfcommSocket);
mRfcommThread.start();
mL2capThread = new SocketAcceptThread(mL2capSocket);
mL2capThread.start();
}
/**
* Called from the AcceptThreads to signal an incoming connection.
* @param device the connecting device.
* @param conSocket the socket associated with the connection.
* @return true if the connection is accepted, false otherwise.
*/
private synchronized boolean onConnect(BluetoothDevice device, BluetoothSocket conSocket) {
if (D) {
Log.d(mTag, "onConnect() socket: " + conSocket);
}
return mConHandler.onConnect(device, conSocket);
}
/**
* Signal to the {@link IObexConnectionHandler} that an error have occurred.
*/
private synchronized void onAcceptFailed() {
shutdown(false);
BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
if ((mAdapter != null) && (mAdapter.getState() == BluetoothAdapter.STATE_ON)) {
Log.d(mTag, "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
*/
public synchronized void shutdown(boolean block) {
if (D) {
Log.d(mTag, "shutdown(block = " + block + ")");
}
if (mRfcommThread != null) {
mRfcommThread.shutdown();
}
if (mL2capThread != null) {
mL2capThread.shutdown();
}
if (block) {
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(mTag, "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
*/
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(mTag, "Accepting socket connection...");
}
connSocket = mServerSocket.accept();
if (D) {
Log.d(mTag, "Accepted socket connection from: " + mServerSocket);
}
if (connSocket == null) {
// TODO: Do we need a max error count, to avoid spinning?
Log.w(mTag, "connSocket is null - reattempt accept");
continue;
}
device = connSocket.getRemoteDevice();
if (device == null) {
Log.i(mTag, "getRemoteDevice() = null - reattempt accept");
try {
connSocket.close();
} catch (IOException e) {
Log.w(mTag, "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) {
/* Close connection if we already have a connection with another device
* by responding to the OBEX connect request.
*/
Log.i(mTag, "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) {
// Expected exception because of shutdown.
} else {
Log.w(mTag, "Accept exception for " + mServerSocket, ex);
ObexServerSockets.this.onAcceptFailed();
}
mStopped = true;
}
} // End while()
} finally {
if (D) {
Log.d(mTag, "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) {
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(mTag, "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(mTag, "shutdown called from another thread - interrupt().");
}
interrupt();
}
}
}
}