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