blob: 3a4fdf6f156af3bf5171bf0ffeffeabd0556adf7 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* 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.libraries.testing.deviceshadower.internal.bluetooth.connection;
import android.os.ParcelFileDescriptor;
import android.os.ParcelUuid;
import com.android.internal.annotations.VisibleForTesting;
import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.BluetoothConstants;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PageScanHandler.ConnectionRequest;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.PhysicalLink.RfcommSocketConnection;
import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.SdpHandler.ServiceRecord;
import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
import com.google.errorprone.annotations.FormatMethod;
import org.robolectric.util.ReflectionHelpers;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Delegate for Bluetooth Rfcommon operations, including creating service record, establishing
* connection, and data communications.
* <p>Socket connection with uuid is supported. Listen on port and connect to port are not
* supported.</p>
*/
public class RfcommDelegate {
private static final Logger LOGGER = Logger.create("RfcommDelegate");
private static final Object LOCK = new Object();
/**
* Callback for Rfcomm operations
*/
public interface Callback {
void onConnectionStateChange(String remoteAddress, boolean isConnected);
}
public static void reset() {
PageScanHandler.reset();
FileDescriptorFactory.reset();
}
final Callback mCallback;
private final String mAddress;
private final Interrupter mInterrupter;
private final SdpHandler mSdpHandler;
private final PageScanHandler mPageScanHandler;
private final Map<String, PhysicalLink> mConnectionMap; // remoteAddress : physicalLink
public RfcommDelegate(String address, Callback callback, Interrupter interrupter) {
this.mAddress = address;
this.mCallback = callback;
this.mInterrupter = interrupter;
mSdpHandler = new SdpHandler(address);
mPageScanHandler = PageScanHandler.getInstance();
mConnectionMap = new ConcurrentHashMap<>();
}
@SuppressWarnings("ObjectToString")
public ParcelFileDescriptor createSocketChannel(String serviceName, ParcelUuid uuid) {
ServiceRecord record = mSdpHandler.createServiceRecord(uuid.getUuid(), serviceName);
if (record == null) {
LOGGER.e(
String.format("Address %s: failed to create socket channel, uuid: %s", mAddress,
uuid));
return null;
}
try {
mPageScanHandler.writePort(record.mServerSocketFd, record.mPort);
} catch (InterruptedException e) {
LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
mAddress,
record.mServerSocketFd), e);
return null;
}
return parcelFileDescriptor(record.mServerSocketFd);
}
@SuppressWarnings("ObjectToString")
public ParcelFileDescriptor connectSocket(String remoteAddress, UUID uuid) {
BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(remoteAddress);
if (remote == null) {
LOGGER.e(String.format("Device %s is not defined.", remoteAddress));
return null;
}
ServiceRecord record = remote.getRfcommDelegate().mSdpHandler.lookupChannel(uuid);
if (record == null) {
LOGGER.e(String.format("Address %s: failed to connect socket, uuid: %s", mAddress,
uuid));
return null;
}
FileDescriptor fd = FileDescriptorFactory.getInstance().createFileDescriptor(mAddress);
try {
mPageScanHandler.writePort(fd, record.mPort);
} catch (InterruptedException e) {
LOGGER.e(String.format("Address %s: failed to write port to incoming data, fd: %s",
mAddress,
fd), e);
return null;
}
// establish connection
try {
initiateConnectToServer(fd, record, remoteAddress);
} catch (IOException e) {
LOGGER.e(
String.format("Address %s: fail to initiate connection to server, clientFd: %s",
mAddress, fd), e);
return null;
}
return parcelFileDescriptor(fd);
}
/**
* Creates connection and unblocks server socket.
* <p>ShadowBluetoothSocket calls the method at the end of connect().</p>
*/
public void finishPendingConnection(
String serverAddress, FileDescriptor clientFd, boolean isEncrypted) {
// update states
PhysicalLink physicalChannel = mConnectionMap.get(serverAddress);
if (physicalChannel == null) {
// use class level lock to ensure two RfcommDelegate hold reference to the same Physical
// Link
synchronized (LOCK) {
physicalChannel = mConnectionMap.get(serverAddress);
if (physicalChannel == null) {
physicalChannel = new PhysicalLink(
serverAddress,
FileDescriptorFactory.getInstance().getAddress(clientFd));
addPhysicalChannel(serverAddress, physicalChannel);
BlueletImpl remote = DeviceShadowEnvironmentImpl.getBlueletImpl(serverAddress);
remote.getRfcommDelegate().addPhysicalChannel(mAddress, physicalChannel);
}
}
}
physicalChannel.addConnection(clientFd, mPageScanHandler.getServerFd(clientFd));
if (isEncrypted) {
physicalChannel.encrypt();
}
mPageScanHandler.finishPendingConnection(clientFd);
}
/**
* Process the next {@link ConnectionRequest} to {@link android.bluetooth.BluetoothServerSocket}
* identified by serverSocketFd. This call will block until next connection request is
* available.
*/
@SuppressWarnings("ObjectToString")
public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
throws IOException {
try {
return mPageScanHandler.processNextConnectionRequest(serverSocketFd);
} catch (InterruptedException e) {
throw new IOException(
logError(e, "failed to process next connection request, serverSocketFd: %s",
serverSocketFd),
e);
}
}
/**
* Waits for a connection established.
* <p>ShadowBluetoothServerSocket calls the method at the end of accept(). Ensure that a
* connection is established when accept() returns.</p>
*/
@SuppressWarnings("ObjectToString")
public void waitForConnectionEstablished(FileDescriptor clientFd) throws IOException {
try {
mPageScanHandler.waitForConnectionEstablished(clientFd);
} catch (InterruptedException e) {
throw new IOException(
logError(e, "failed to wait for connection established. clientFd: %s",
clientFd), e);
}
}
@SuppressWarnings("ObjectToString")
public void write(String remoteAddress, FileDescriptor localFd, int b)
throws IOException {
checkInterrupt();
RfcommSocketConnection connection =
mConnectionMap.get(remoteAddress).getConnection(localFd);
if (connection == null) {
throw new IOException("closed");
}
try {
connection.write(remoteAddress, b);
} catch (InterruptedException e) {
throw new IOException(
logError(e, "failed to write to target %s, fd: %s", remoteAddress,
localFd), e);
}
}
@SuppressWarnings("ObjectToString")
public int read(String remoteAddress, FileDescriptor localFd) throws IOException {
checkInterrupt();
// remoteAddress is null: 1. server socket, 2. client socket before connected
try {
if (remoteAddress == null) {
return mPageScanHandler.read(localFd);
}
} catch (InterruptedException e) {
throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
}
RfcommSocketConnection connection =
mConnectionMap.get(remoteAddress).getConnection(localFd);
if (connection == null) {
throw new IOException("closed");
}
try {
return connection.read(mAddress);
} catch (InterruptedException e) {
throw new IOException(logError(e, "failed to read, fd: %s", localFd), e);
}
}
@SuppressWarnings("ObjectToString")
public void shutdownInput(String remoteAddress, FileDescriptor localFd)
throws IOException {
// remoteAddress is null: 1. server socket, 2. client socket before connected
try {
if (remoteAddress == null) {
mPageScanHandler.write(localFd, BluetoothConstants.SOCKET_CLOSE);
return;
}
} catch (InterruptedException e) {
throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
}
RfcommSocketConnection connection =
mConnectionMap.get(remoteAddress).getConnection(localFd);
if (connection == null) {
LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
localFd));
return;
}
try {
connection.write(mAddress, BluetoothConstants.SOCKET_CLOSE);
} catch (InterruptedException e) {
throw new IOException(logError(e, "failed to shutdown input. fd: %s", localFd), e);
}
}
@SuppressWarnings("ObjectToString")
public void shutdownOutput(String remoteAddress, FileDescriptor localFd)
throws IOException {
RfcommSocketConnection connection =
mConnectionMap.get(remoteAddress).getConnection(localFd);
if (connection == null) {
LOGGER.d(String.format("Address %s: Connection already closed. fd: %s.", mAddress,
localFd));
return;
}
try {
connection.write(remoteAddress, BluetoothConstants.SOCKET_CLOSE);
} catch (InterruptedException e) {
throw new IOException(logError(e, "failed to shutdown output. fd: %s", localFd), e);
}
}
@SuppressWarnings("ObjectToString")
public void closeServerSocket(FileDescriptor serverSocketFd) throws IOException {
// remove service record
UUID uuid = mSdpHandler.getUuid(serverSocketFd);
mSdpHandler.removeServiceRecord(uuid);
// unblock accept()
try {
mPageScanHandler.cancelServerSocket(serverSocketFd);
} catch (InterruptedException e) {
throw new IOException(
logError(e, "failed to cancel server socket, serverSocketFd: %s",
serverSocketFd),
e);
}
}
public FileDescriptor getServerFd(FileDescriptor clientFd) {
return mPageScanHandler.getServerFd(clientFd);
}
@VisibleForTesting
public void addPhysicalChannel(String remoteAddress, PhysicalLink channel) {
mConnectionMap.put(remoteAddress, channel);
}
@SuppressWarnings("ObjectToString")
public void initiateConnectToClient(FileDescriptor clientFd, int port)
throws IOException {
checkInterrupt();
String clientAddress = FileDescriptorFactory.getInstance().getAddress(clientFd);
LOGGER.d(String.format("Address %s: init connection to %s, clientFd: %s",
mAddress, clientAddress, clientFd));
try {
mPageScanHandler.writeInitialConnectionInfo(clientFd, mAddress, port);
} catch (InterruptedException e) {
throw new IOException(
logError(e,
"failed to write initial connection info to %s, clientFd: %s",
clientAddress, clientFd),
e);
}
}
@SuppressWarnings("ObjectToString")
private void initiateConnectToServer(FileDescriptor clientFd, ServiceRecord serviceRecord,
String serverAddress) throws IOException {
checkInterrupt();
LOGGER.d(
String.format("Address %s: init connection to %s, serverSocketFd: %s, clientFd: %s",
mAddress, serverAddress, serviceRecord.mServerSocketFd, clientFd));
try {
ConnectionRequest request = new ConnectionRequest(clientFd, mAddress, serverAddress,
serviceRecord.mPort);
mPageScanHandler.postConnectionRequest(serviceRecord.mServerSocketFd, request);
} catch (InterruptedException e) {
throw new IOException(
logError(e,
"failed to post connection request, serverSocketFd: %s, "
+ "clientFd: %s",
serviceRecord.mServerSocketFd, clientFd),
e);
}
}
public void checkInterrupt() throws IOException {
mInterrupter.checkInterrupt();
}
private ParcelFileDescriptor parcelFileDescriptor(FileDescriptor fd) {
return ReflectionHelpers.callConstructor(ParcelFileDescriptor.class,
ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd));
}
@FormatMethod
private String logError(Exception e, String msgTmpl, Object... args) {
String errMsg = String.format("Address %s: ", mAddress) + String.format(msgTmpl, args);
LOGGER.e(errMsg, e);
return errMsg;
}
}