ObexServerSockets.java revision cecda8201dad0b6d2007f6c7e31b1113729d7850
1/*
2* Copyright (C) 2015 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15package com.android.bluetooth;
16
17import java.io.IOException;
18
19import javax.obex.ResponseCodes;
20import javax.obex.ServerSession;
21
22import android.bluetooth.BluetoothAdapter;
23import android.bluetooth.BluetoothDevice;
24import android.bluetooth.BluetoothServerSocket;
25import android.bluetooth.BluetoothSocket;
26import android.util.Log;
27
28/**
29 * Wraps multiple BluetoothServerSocket objects to make it possible to accept connections on
30 * both a RFCOMM and L2CAP channel in parallel.<br>
31 * Create an instance using {@link #create()}, which will block until the sockets have been created
32 * and channel numbers have been assigned.<br>
33 * Use {@link #getRfcommChannel()} and {@link #getL2capPsm()} to get the channel numbers to
34 * put into the SDP record.<br>
35 * Call {@link #shutdown(boolean)} to terminate the accept threads created by the call to
36 * {@link #create(IObexConnectionHandler)}.<br>
37 * A reference to an object of this type cannot be reused, and the {@link BluetoothServerSocket}
38 * object references passed to this object will be closed by this object, hence cannot be reused
39 * either (This is needed, as the only way to interrupt an accept call is to close the socket...)
40 * <br>
41 * When a connection is accepted,
42 * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
43 * If the an error occur while waiting for an incoming connection
44 * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
45 * In both cases the {@link ObexServerSockets} object have terminated, and a new must be created.
46 */
47public class ObexServerSockets {
48    private final String TAG;
49    private static final String STAG = "ObexServerSockets";
50    private static final boolean D = true; // TODO: set to false!
51    private static final int NUMBER_OF_SOCKET_TYPES = 2; // increment if LE will be supported
52
53    private final IObexConnectionHandler mConHandler;
54    /* The wrapped sockets */
55    private final BluetoothServerSocket mRfcommSocket;
56    private final BluetoothServerSocket mL2capSocket;
57    /* Handles to the accept threads. Needed for shutdown. */
58    private SocketAcceptThread mRfcommThread = null;
59    private SocketAcceptThread mL2capThread = null;
60
61    private volatile boolean mConAccepted = false;
62
63    private static volatile int sInstanceCounter = 0;
64
65    private ObexServerSockets(IObexConnectionHandler conHandler,
66            BluetoothServerSocket rfcommSocket,
67            BluetoothServerSocket l2capSocket) {
68        mConHandler = conHandler;
69        mRfcommSocket = rfcommSocket;
70        mL2capSocket = l2capSocket;
71        TAG = "ObexServerSockets" + sInstanceCounter++;
72    }
73
74    /**
75     * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket}
76     * @param validator a reference to the {@link IObexConnectionHandler} object to call
77     *                  to validate an incoming connection.
78     * @return a reference to a {@link ObexServerSockets} object instance.
79     * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s.
80     */
81    public static ObexServerSockets create(IObexConnectionHandler validator) {
82        return create(validator, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP,
83                BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, true);
84    }
85
86    /**
87     * Creates an Insecure RFCOMM {@link BluetoothServerSocket} and a L2CAP
88         *                  {@link BluetoothServerSocket}
89     * @param validator a reference to the {@link IObexConnectionHandler} object to call
90     *                  to validate an incoming connection.
91     * @return a reference to a {@link ObexServerSockets} object instance.
92     * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s.
93     */
94    public static ObexServerSockets createInsecure(IObexConnectionHandler validator) {
95        return create(validator, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP,
96                BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false);
97    }
98
99    /**
100     * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket}
101     * with specific l2cap and RFCOMM channel numbers. It is the responsibility of the caller to
102     * ensure the numbers are free and can be used, e.g. by calling {@link #getL2capPsm()} and
103     * {@link #getRfcommChannel()} in {@link ObexServerSockets}.
104     * @param validator a reference to the {@link IObexConnectionHandler} object to call
105     *                  to validate an incoming connection.
106     * @param isSecure boolean flag to determine whther socket would be secured or inseucure.
107     * @return a reference to a {@link ObexServerSockets} object instance.
108     * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s.
109     *
110     * TODO: Make public when it becomes possible to determine that the listen-call
111     *       failed due to channel-in-use.
112     */
113    private static ObexServerSockets create(
114            IObexConnectionHandler validator, int rfcommChannel, int l2capPsm, boolean isSecure) {
115        if(D) Log.d(STAG,"create(rfcomm = " +rfcommChannel + ", l2capPsm = " + l2capPsm +")");
116        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
117        if(bt == null) {
118            throw new RuntimeException("No bluetooth adapter...");
119        }
120        BluetoothServerSocket rfcommSocket = null;
121        BluetoothServerSocket l2capSocket = null;
122        boolean initSocketOK = false;
123        final int CREATE_RETRY_TIME = 10;
124
125        // It's possible that create will fail in some cases. retry for 10 times
126        for (int i = 0; i < CREATE_RETRY_TIME; i++) {
127            initSocketOK = true;
128            try {
129                if(rfcommSocket == null) {
130                    if (isSecure) {
131                        rfcommSocket = bt.listenUsingRfcommOn(rfcommChannel);
132                    } else {
133                        rfcommSocket = bt.listenUsingInsecureRfcommOn(rfcommChannel);
134                    }
135                }
136                if(l2capSocket == null) {
137                    if (isSecure) {
138                        l2capSocket = bt.listenUsingL2capOn(l2capPsm);
139                    } else {
140                        l2capSocket = bt.listenUsingInsecureL2capOn(l2capPsm);
141                    }
142                }
143            } catch (IOException e) {
144                Log.e(STAG, "Error create ServerSockets ",e);
145                initSocketOK = false;
146            }
147            if (!initSocketOK) {
148                // Need to break out of this loop if BT is being turned off.
149                int state = bt.getState();
150                if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
151                    (state != BluetoothAdapter.STATE_ON)) {
152                    Log.w(STAG, "initServerSockets failed as BT is (being) turned off");
153                    break;
154                }
155                try {
156                    if (D) Log.v(STAG, "waiting 300 ms...");
157                    Thread.sleep(300);
158                } catch (InterruptedException e) {
159                    Log.e(STAG, "create() was interrupted");
160                }
161            } else {
162                break;
163            }
164        }
165
166        if (initSocketOK) {
167            if (D) Log.d(STAG, "Succeed to create listening sockets ");
168            ObexServerSockets sockets = new ObexServerSockets(validator, rfcommSocket, l2capSocket);
169            sockets.startAccept();
170            return sockets;
171        } else {
172            Log.e(STAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
173            return null;
174        }
175    }
176
177    /**
178     * Returns the channel number assigned to the RFCOMM socket. This will be a static value, that
179     * should be reused for multiple connections.
180     * @return the RFCOMM channel number
181     */
182    public int getRfcommChannel() {
183        return mRfcommSocket.getChannel();
184    }
185
186    /**
187     * Returns the channel number assigned to the L2CAP socket. This will be a static value, that
188     * should be reused for multiple connections.
189     * @return the L2CAP channel number
190     */
191    public int getL2capPsm() {
192        return mL2capSocket.getChannel();
193    }
194
195    /**
196     * Initiate the accept threads.
197     * Will create a thread for each socket type. an incoming connection will be signaled to
198     * the {@link IObexConnectionValidator#onConnect()}, at which point both threads will exit.
199     */
200    private void startAccept() {
201        if(D) Log.d(TAG,"startAccept()");
202        prepareForNewConnect();
203
204        mRfcommThread = new SocketAcceptThread(mRfcommSocket);
205        mRfcommThread.start();
206
207        mL2capThread = new SocketAcceptThread(mL2capSocket);
208        mL2capThread.start();
209    }
210
211    /**
212     * Set state to accept new incoming connection. Will cause the next incoming connection to be
213     * Signaled through {@link IObexConnectionValidator#onConnect()};
214     */
215    synchronized public void prepareForNewConnect() {
216        if(D) Log.d(TAG, "prepareForNewConnect()");
217        mConAccepted = false;
218    }
219
220    /**
221     * Called from the AcceptThreads to signal an incoming connection.
222     * This is the entry point that needs to synchronize between the accept
223     * threads, and ensure only a single connection is accepted.
224     * {@link mAcceptedSocket} is used a state variable.
225     * @param device the connecting device.
226     * @param conSocket the socket associated with the connection.
227     * @return true if the connection is accepted, false otherwise.
228     */
229    synchronized private boolean onConnect(BluetoothDevice device, BluetoothSocket conSocket) {
230        if(D) Log.d(TAG, "onConnect() socket: " + conSocket + " mConAccepted = " + mConAccepted);
231        if(mConAccepted  == false && mConHandler.onConnect(device, conSocket) == true) {
232            mConAccepted = true; // TODO: Reset this when ready to accept new connection
233            /* Signal the remaining threads to stop.
234            shutdown(false); */ // UPDATE: TODO: remove - redesigned to keep running...
235            return true;
236        }
237        return false;
238    }
239
240    /**
241     * Signal to the {@link IObexConnectionHandler} that an error have occurred.
242     */
243    synchronized private void onAcceptFailed() {
244        shutdown(false);
245        BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
246        if ((mAdapter != null) && (mAdapter.getState() == BluetoothAdapter.STATE_ON)) {
247            Log.d(TAG, "onAcceptFailed() calling shutdown...");
248            mConHandler.onAcceptFailed();
249        }
250    }
251
252    /**
253     * Terminate any running accept threads
254     * @param block Set true to block the calling thread until the AcceptThreads
255     * has ended execution
256     */
257    synchronized public void shutdown(boolean block) {
258        if(D) Log.d(TAG, "shutdown(block = " + block + ")");
259        if(mRfcommThread != null) {
260            mRfcommThread.shutdown();
261        }
262        if(mL2capThread != null){
263            mL2capThread.shutdown();
264        }
265        if(block == true) {
266            while(mRfcommThread != null || mL2capThread != null) {
267                try {
268                    if(mRfcommThread != null) {
269                        mRfcommThread.join();
270                        mRfcommThread = null;
271                    }
272                    if(mL2capThread != null) {
273                        mL2capThread.join();
274                        mL2capThread = null;
275                    }
276                } catch (InterruptedException e) {
277                    Log.i(TAG, "shutdown() interrupted, continue waiting...", e);
278                }
279            }
280        } else {
281            mRfcommThread = null;
282            mL2capThread = null;
283        }
284    }
285
286    /**
287     * A thread that runs in the background waiting for remote an incoming
288     * connect. Once a remote socket connects, this thread will be
289     * shutdown. When the remote disconnect, this thread shall be restarted to
290     * accept a new connection.
291     */
292    private class SocketAcceptThread extends Thread {
293
294        private boolean mStopped = false;
295        private final BluetoothServerSocket mServerSocket;
296
297        /**
298         * Create a SocketAcceptThread
299         * @param serverSocket shall never be null.
300         * @param latch shall never be null.
301         * @throws IllegalArgumentException
302         */
303        public SocketAcceptThread(BluetoothServerSocket serverSocket) {
304            if(serverSocket == null) {
305                throw new IllegalArgumentException("serverSocket cannot be null");
306            }
307            mServerSocket = serverSocket;
308        }
309
310        /**
311         * Run until shutdown of BT.
312         * Accept incoming connections and reject if needed. Keep accepting incoming connections.
313         */
314        @Override
315        public void run() {
316            try {
317                while (!mStopped) {
318                    BluetoothSocket connSocket;
319                    BluetoothDevice device;
320
321                    try {
322                        if (D) Log.d(TAG, "Accepting socket connection...");
323
324                        connSocket = mServerSocket.accept();
325                        if (D) Log.d(TAG, "Accepted socket connection from: " + mServerSocket);
326
327                       if (connSocket == null) {
328                           // TODO: Do we need a max error count, to avoid spinning?
329                            Log.w(TAG, "connSocket is null - reattempt accept");
330                            continue;
331                        }
332                        device = connSocket.getRemoteDevice();
333
334                        if (device == null) {
335                            Log.i(TAG, "getRemoteDevice() = null - reattempt accept");
336                            try{
337                                connSocket.close();
338                            } catch (IOException e) {
339                                Log.w(TAG, "Error closing the socket. ignoring...",e );
340                            }
341                            continue;
342                        }
343
344                        /* Signal to the service that we have received an incoming connection.
345                         */
346                        boolean isValid = ObexServerSockets.this.onConnect(device, connSocket);
347
348                        if(isValid == false) {
349                            /* Close connection if we already have a connection with another device
350                             * by responding to the OBEX connect request.
351                             */
352                            Log.i(TAG, "RemoteDevice is invalid - creating ObexRejectServer.");
353                            BluetoothObexTransport obexTrans =
354                                    new BluetoothObexTransport(connSocket);
355                            // Create and detach a selfdestructing ServerSession to respond to any
356                            // incoming OBEX signals.
357                            new ServerSession(obexTrans,
358                                    new ObexRejectServer(
359                                            ResponseCodes.OBEX_HTTP_UNAVAILABLE,
360                                            connSocket),
361                                    null);
362                            // now wait for a new connect
363                        } else {
364                            // now wait for a new connect
365                        }
366                    } catch (IOException ex) {
367                        if(mStopped == true) {
368                            // Expected exception because of shutdown.
369                        } else {
370                            Log.w(TAG, "Accept exception for " +
371                                    mServerSocket, ex);
372                            ObexServerSockets.this.onAcceptFailed();
373                        }
374                        mStopped=true;
375                    }
376                } // End while()
377            } finally {
378                if (D) Log.d(TAG, "AcceptThread ended for: " + mServerSocket);
379            }
380        }
381
382        /**
383         * Shuts down the accept threads, and closes the ServerSockets, causing all related
384         * BluetoothSockets to disconnect, hence do not call until all all accepted connections
385         * are ready to be disconnected.
386         */
387        public void shutdown() {
388            if(mStopped == false) {
389                mStopped = true;
390                // TODO: According to the documentation, this should not close the accepted
391                //       sockets - and that is true, but it closes the l2cap connections, and
392                //       therefore it implicitly also closes the accepted sockets...
393                try {
394                     mServerSocket.close();
395                } catch (IOException e) {
396                    if(D) Log.d(TAG, "Exception while thread shutdown:", e);
397                }
398            }
399            // If called from another thread, interrupt the thread
400            if(!Thread.currentThread().equals(this)){
401                // TODO: Will this interrupt the thread if it is blocked in synchronized?
402                // Else: change to use InterruptableLock
403                if(D) Log.d(TAG, "shutdown called from another thread - interrupt().");
404                interrupt();
405            }
406        }
407    }
408}
409