| /* |
| * 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; |
| } catch (SecurityException e) { |
| Log.e(STAG, "Error create ServerSockets ", e); |
| initSocketOK = false; |
| break; |
| } |
| 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(); |
| } |
| } |
| } |
| } |