• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.libraries.testing.deviceshadower.internal.bluetooth.connection;
18 
19 import android.os.ParcelFileDescriptor;
20 import android.os.ParcelUuid;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
24 import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
25 import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
26 import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PageScanHandler.ConnectionRequest;
27 import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PhysicalLink.RfcommSocketConnection;
28 import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.SdpHandler.ServiceRecord;
29 import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
30 import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
31 
32 import com.google.errorprone.annotations.FormatMethod;
33 
34 import org.robolectric.util.ReflectionHelpers;
35 
36 import java.io.FileDescriptor;
37 import java.io.IOException;
38 import java.util.Map;
39 import java.util.UUID;
40 import java.util.concurrent.ConcurrentHashMap;
41 
42 /**
43  * Delegate for Bluetooth Rfcommon operations, including creating service record, establishing
44  * connection, and data communications.
45  * <p>Socket connection with uuid is supported. Listen on port and connect to port are not
46  * supported.</p>
47  */
48 public class RfcommDelegate {
49 
50     private static final Logger LOGGER = Logger.create("RfcommDelegate");
51     private static final Object LOCK = new Object();
52 
53     /**
54      * Callback for Rfcomm operations
55      */
56     public interface Callback {
57 
onConnectionStateChange(String remoteAddress, boolean isConnected)58         void onConnectionStateChange(String remoteAddress, boolean isConnected);
59     }
60 
reset()61     public static void reset() {
62         PageScanHandler.reset();
63         FileDescriptorFactory.reset();
64     }
65 
66     final Callback mCallback;
67     private final String mAddress;
68     private final Interrupter mInterrupter;
69     private final SdpHandler mSdpHandler;
70     private final PageScanHandler mPageScanHandler;
71     private final Map<String, PhysicalLink> mConnectionMap; // remoteAddress : physicalLink
72 
RfcommDelegate(String address, Callback callback, Interrupter interrupter)73     public RfcommDelegate(String address, Callback callback, Interrupter interrupter) {
74         this.mAddress = address;
75         this.mCallback = callback;
76         this.mInterrupter = interrupter;
77         mSdpHandler = new SdpHandler(address);
78         mPageScanHandler = PageScanHandler.getInstance();
79         mConnectionMap = new ConcurrentHashMap<>();
80     }
81 
82     @SuppressWarnings("ObjectToString")
createSocketChannel(String serviceName, ParcelUuid uuid)83     public ParcelFileDescriptor createSocketChannel(String serviceName, ParcelUuid uuid) {
84         ServiceRecord record = mSdpHandler.createServiceRecord(uuid.getUuid(), serviceName);
85         if (record == null) {
86             LOGGER.e(
87                     String.format("Address %s: failed to create socket channel, uuid: %s", mAddress,
88                             uuid));
89             return null;
90         }
91         try {
92             mPageScanHandler.writePort(record.mServerSocketFd, record.mPort);
93         } catch (InterruptedException e) {
94             LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
95                     mAddress,
96                     record.mServerSocketFd), e);
97             return null;
98         }
99         return parcelFileDescriptor(record.mServerSocketFd);
100     }
101 
102     @SuppressWarnings("ObjectToString")
connectSocket(String remoteAddress, UUID uuid)103     public ParcelFileDescriptor connectSocket(String remoteAddress, UUID uuid) {
104         BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(remoteAddress);
105         if (remote == null) {
106             LOGGER.e(String.format("Device %s is not defined.", remoteAddress));
107             return null;
108         }
109         ServiceRecord record = remote.getRfcommDelegate().mSdpHandler.lookupChannel(uuid);
110         if (record == null) {
111             LOGGER.e(String.format("Address %s: failed to connect socket, uuid: %s", mAddress,
112                     uuid));
113             return null;
114         }
115         FileDescriptor fd = FileDescriptorFactory.getInstance().createFileDescriptor(mAddress);
116         try {
117             mPageScanHandler.writePort(fd, record.mPort);
118         } catch (InterruptedException e) {
119             LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
120                     mAddress,
121                     fd), e);
122             return null;
123         }
124 
125         // establish connection
126         try {
127             initiateConnectToServer(fd, record, remoteAddress);
128         } catch (IOException e) {
129             LOGGER.e(
130                     String.format("Address %s: fail to initiate connection to server, clientFd: %s",
131                             mAddress, fd), e);
132             return null;
133         }
134         return parcelFileDescriptor(fd);
135     }
136 
137     /**
138      * Creates connection and unblocks server socket.
139      * <p>ShadowBluetoothSocket calls the method at the end of connect().</p>
140      */
finishPendingConnection( String serverAddress, FileDescriptor clientFd, boolean isEncrypted)141     public void finishPendingConnection(
142             String serverAddress, FileDescriptor clientFd, boolean isEncrypted) {
143         // update states
144         PhysicalLink physicalChannel = mConnectionMap.get(serverAddress);
145         if (physicalChannel == null) {
146             // use class level lock to ensure two RfcommDelegate hold reference to the same Physical
147             // Link
148             synchronized (LOCK) {
149                 physicalChannel = mConnectionMap.get(serverAddress);
150                 if (physicalChannel == null) {
151                     physicalChannel = new PhysicalLink(
152                             serverAddress,
153                             FileDescriptorFactory.getInstance().getAddress(clientFd));
154                     addPhysicalChannel(serverAddress, physicalChannel);
155                     BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(serverAddress);
156                     remote.getRfcommDelegate().addPhysicalChannel(mAddress, physicalChannel);
157                 }
158             }
159         }
160         physicalChannel.addConnection(clientFd, mPageScanHandler.getServerFd(clientFd));
161 
162         if (isEncrypted) {
163             physicalChannel.encrypt();
164         }
165         mPageScanHandler.finishPendingConnection(clientFd);
166     }
167 
168     /**
169      * Process the next {@link ConnectionRequest} to {@link android.bluetooth.BluetoothServerSocket}
170      * identified by serverSocketFd. This call will block until next connection request is
171      * available.
172      */
173     @SuppressWarnings("ObjectToString")
processNextConnectionRequest(FileDescriptor serverSocketFd)174     public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
175             throws IOException {
176         try {
177             return mPageScanHandler.processNextConnectionRequest(serverSocketFd);
178         } catch (InterruptedException e) {
179             throw new IOException(
180                     logError(e, "failed to process next connection request, serverSocketFd: %s",
181                             serverSocketFd),
182                     e);
183         }
184     }
185 
186     /**
187      * Waits for a connection established.
188      * <p>ShadowBluetoothServerSocket calls the method at the end of accept(). Ensure that a
189      * connection is established when accept() returns.</p>
190      */
191     @SuppressWarnings("ObjectToString")
waitForConnectionEstablished(FileDescriptor clientFd)192     public void waitForConnectionEstablished(FileDescriptor clientFd) throws IOException {
193         try {
194             mPageScanHandler.waitForConnectionEstablished(clientFd);
195         } catch (InterruptedException e) {
196             throw new IOException(
197                     logError(e, "failed to wait for connection established. clientFd: %s",
198                             clientFd), e);
199         }
200     }
201 
202     @SuppressWarnings("ObjectToString")
write(String remoteAddress, FileDescriptor localFd, int b)203     public void write(String remoteAddress, FileDescriptor localFd, int b)
204             throws IOException {
205         checkInterrupt();
206         RfcommSocketConnection connection =
207                 mConnectionMap.get(remoteAddress).getConnection(localFd);
208         if (connection == null) {
209             throw new IOException("closed");
210         }
211         try {
212             connection.write(remoteAddress, b);
213         } catch (InterruptedException e) {
214             throw new IOException(
215                     logError(e, "failed to write to target %s, fd: %s", remoteAddress,
216                             localFd), e);
217         }
218     }
219 
220     @SuppressWarnings("ObjectToString")
read(String remoteAddress, FileDescriptor localFd)221     public int read(String remoteAddress, FileDescriptor localFd) throws IOException {
222         checkInterrupt();
223         // remoteAddress is null: 1. server socket, 2. client socket before connected
224         try {
225             if (remoteAddress == null) {
226                 return mPageScanHandler.read(localFd);
227             }
228         } catch (InterruptedException e) {
229             throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
230         }
231 
232         RfcommSocketConnection connection =
233                 mConnectionMap.get(remoteAddress).getConnection(localFd);
234         if (connection == null) {
235             throw new IOException("closed");
236         }
237         try {
238             return connection.read(mAddress);
239         } catch (InterruptedException e) {
240             throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
241         }
242     }
243 
244     @SuppressWarnings("ObjectToString")
shutdownInput(String remoteAddress, FileDescriptor localFd)245     public void shutdownInput(String remoteAddress, FileDescriptor localFd)
246             throws IOException {
247         // remoteAddress is null: 1. server socket, 2. client socket before connected
248         try {
249             if (remoteAddress == null) {
250                 mPageScanHandler.write(localFd, BluetoothConstants.SOCKET_CLOSE);
251                 return;
252             }
253         } catch (InterruptedException e) {
254             throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
255         }
256 
257         RfcommSocketConnection connection =
258                 mConnectionMap.get(remoteAddress).getConnection(localFd);
259         if (connection == null) {
260             LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
261                     localFd));
262             return;
263         }
264         try {
265             connection.write(mAddress, BluetoothConstants.SOCKET_CLOSE);
266         } catch (InterruptedException e) {
267             throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
268         }
269     }
270 
271     @SuppressWarnings("ObjectToString")
shutdownOutput(String remoteAddress, FileDescriptor localFd)272     public void shutdownOutput(String remoteAddress, FileDescriptor localFd)
273             throws IOException {
274         RfcommSocketConnection connection =
275                 mConnectionMap.get(remoteAddress).getConnection(localFd);
276         if (connection == null) {
277             LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
278                     localFd));
279             return;
280         }
281         try {
282             connection.write(remoteAddress, BluetoothConstants.SOCKET_CLOSE);
283         } catch (InterruptedException e) {
284             throw new IOException(logError(e, "failed to shutdown output. fd: %s", localFd), e);
285         }
286     }
287 
288     @SuppressWarnings("ObjectToString")
closeServerSocket(FileDescriptor serverSocketFd)289     public void closeServerSocket(FileDescriptor serverSocketFd) throws IOException {
290         // remove service record
291         UUID uuid = mSdpHandler.getUuid(serverSocketFd);
292         mSdpHandler.removeServiceRecord(uuid);
293         // unblock accept()
294         try {
295             mPageScanHandler.cancelServerSocket(serverSocketFd);
296         } catch (InterruptedException e) {
297             throw new IOException(
298                     logError(e, "failed to cancel server socket, serverSocketFd: %s",
299                             serverSocketFd),
300                     e);
301         }
302     }
303 
getServerFd(FileDescriptor clientFd)304     public FileDescriptor getServerFd(FileDescriptor clientFd) {
305         return mPageScanHandler.getServerFd(clientFd);
306     }
307 
308     @VisibleForTesting
addPhysicalChannel(String remoteAddress, PhysicalLink channel)309     public void addPhysicalChannel(String remoteAddress, PhysicalLink channel) {
310         mConnectionMap.put(remoteAddress, channel);
311     }
312 
313     @SuppressWarnings("ObjectToString")
initiateConnectToClient(FileDescriptor clientFd, int port)314     public void initiateConnectToClient(FileDescriptor clientFd, int port)
315             throws IOException {
316         checkInterrupt();
317         String clientAddress = FileDescriptorFactory.getInstance().getAddress(clientFd);
318         LOGGER.d(String.format("Address %s: init connection to %s, clientFd: %s",
319                 mAddress, clientAddress, clientFd));
320         try {
321             mPageScanHandler.writeInitialConnectionInfo(clientFd, mAddress, port);
322         } catch (InterruptedException e) {
323             throw new IOException(
324                     logError(e,
325                             "failed to write initial connection info to %s, clientFd: %s",
326                             clientAddress, clientFd),
327                     e);
328         }
329     }
330 
331     @SuppressWarnings("ObjectToString")
initiateConnectToServer(FileDescriptor clientFd, ServiceRecord serviceRecord, String serverAddress)332     private void initiateConnectToServer(FileDescriptor clientFd, ServiceRecord serviceRecord,
333             String serverAddress) throws IOException {
334         checkInterrupt();
335         LOGGER.d(
336                 String.format("Address %s: init connection to %s, serverSocketFd: %s, clientFd: %s",
337                         mAddress, serverAddress, serviceRecord.mServerSocketFd, clientFd));
338         try {
339             ConnectionRequest request = new ConnectionRequest(clientFd, mAddress, serverAddress,
340                     serviceRecord.mPort);
341             mPageScanHandler.postConnectionRequest(serviceRecord.mServerSocketFd, request);
342         } catch (InterruptedException e) {
343             throw new IOException(
344                     logError(e,
345                             "failed to post connection request, serverSocketFd: %s, "
346                                     + "clientFd: %s",
347                             serviceRecord.mServerSocketFd, clientFd),
348                     e);
349         }
350     }
351 
checkInterrupt()352     public void checkInterrupt() throws IOException {
353         mInterrupter.checkInterrupt();
354     }
355 
parcelFileDescriptor(FileDescriptor fd)356     private ParcelFileDescriptor parcelFileDescriptor(FileDescriptor fd) {
357         return ReflectionHelpers.callConstructor(ParcelFileDescriptor.class,
358                 ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd));
359     }
360 
361     @FormatMethod
logError(Exception e, String msgTmpl, Object... args)362     private String logError(Exception e, String msgTmpl, Object... args) {
363         String errMsg = String.format("Address %s: ", mAddress) + String.format(msgTmpl, args);
364         LOGGER.e(errMsg, e);
365         return errMsg;
366     }
367 }
368