blob: 82b97ff88cacd95ae321a120764bad76e999dfde [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.Build.VERSION;
import com.android.internal.annotations.VisibleForTesting;
import com.android.libraries.testing.deviceshadower.internal.utils.MacAddressGenerator;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Encapsulate page scan operations -- handle connection establishment between Bluetooth devices.
*/
public class PageScanHandler {
private static final ConnectionRequest REQUEST_SERVER_SOCKET_CLOSE = new ConnectionRequest();
private static PageScanHandler sInstance = null;
public static synchronized PageScanHandler getInstance() {
if (sInstance == null) {
sInstance = new PageScanHandler();
}
return sInstance;
}
public static synchronized void reset() {
sInstance = null;
}
// use FileDescriptor to identify incoming data before socket is connected.
private final Map<FileDescriptor, BlockingQueue<Integer>> mIncomingDataMap;
// map a server socket fd to a connection request queue
private final Map<FileDescriptor, BlockingQueue<ConnectionRequest>> mConnectionRequests;
// map a fd on client side to a fd of BluetoothSocket(not BluetoothServerSocket) on server side
private final Map<FileDescriptor, FileDescriptor> mClientServerFdMap;
// map a client fd to a connection request so the client socket can finish the pending
// connection
private final Map<FileDescriptor, ConnectionRequest> mPendingConnections;
private PageScanHandler() {
mIncomingDataMap = new ConcurrentHashMap<>();
mConnectionRequests = new ConcurrentHashMap<>();
mClientServerFdMap = new ConcurrentHashMap<>();
mPendingConnections = new ConcurrentHashMap<>();
}
public void postConnectionRequest(FileDescriptor serverSocketFd, ConnectionRequest request)
throws InterruptedException {
// used by the returning socket on server-side
FileDescriptor fd = FileDescriptorFactory.getInstance()
.createFileDescriptor(request.mServerAddress);
mClientServerFdMap.put(request.mClientFd, fd);
BlockingQueue<ConnectionRequest> requests = mConnectionRequests.get(serverSocketFd);
requests.put(request);
mPendingConnections.put(request.mClientFd, request);
}
public void addServerSocket(FileDescriptor serverSocketFd) {
mConnectionRequests.put(serverSocketFd, new LinkedBlockingQueue<ConnectionRequest>());
}
public FileDescriptor getServerFd(FileDescriptor clientFd) {
return mClientServerFdMap.get(clientFd);
}
// TODO(b/79994182): see go/objecttostring-lsc
@SuppressWarnings("ObjectToString")
public FileDescriptor processNextConnectionRequest(FileDescriptor serverSocketFd)
throws IOException, InterruptedException {
ConnectionRequest request = mConnectionRequests.get(serverSocketFd).take();
if (request == REQUEST_SERVER_SOCKET_CLOSE) {
// TODO(b/79994182): FileDescriptor does not implement toString() in serverSocketFd
throw new IOException("Server socket is closed. fd: " + serverSocketFd);
}
writeInitialConnectionInfo(serverSocketFd, request.mClientAddress, request.mPort);
return request.mClientFd;
}
public void waitForConnectionEstablished(FileDescriptor clientFd) throws InterruptedException {
ConnectionRequest request = mPendingConnections.get(clientFd);
if (request != null) {
request.mCountDownLatch.await();
}
}
public void finishPendingConnection(FileDescriptor clientFd) {
ConnectionRequest request = mPendingConnections.get(clientFd);
if (request != null) {
request.mCountDownLatch.countDown();
}
}
public void cancelServerSocket(FileDescriptor serverSocketFd) throws InterruptedException {
mConnectionRequests.get(serverSocketFd).put(REQUEST_SERVER_SOCKET_CLOSE);
}
public void writeInitialConnectionInfo(FileDescriptor fd, String address, int port)
throws InterruptedException {
for (byte b : initialConnectionInfo(address, port)) {
write(fd, Integer.valueOf(b));
}
}
public void writePort(FileDescriptor fd, int port) throws InterruptedException {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(port).array();
for (byte b : bytes) {
write(fd, Integer.valueOf(b));
}
}
public void write(FileDescriptor fd, int data) throws InterruptedException {
BlockingQueue<Integer> incomingData = mIncomingDataMap.get(fd);
if (incomingData == null) {
synchronized (mIncomingDataMap) {
incomingData = mIncomingDataMap.get(fd);
if (incomingData == null) {
incomingData = new LinkedBlockingQueue<Integer>();
mIncomingDataMap.put(fd, incomingData);
}
}
}
incomingData.put(data);
}
public int read(FileDescriptor fd) throws InterruptedException {
return mIncomingDataMap.get(fd).take();
}
/**
* A connection request from a {@link android.bluetooth.BluetoothSocket}.
*/
@VisibleForTesting
public static class ConnectionRequest {
final FileDescriptor mClientFd;
final String mClientAddress;
final String mServerAddress;
final int mPort;
final CountDownLatch mCountDownLatch; // block server socket until connection established
public ConnectionRequest(FileDescriptor fd, String clientAddress, String serverAddress,
int port) {
mClientFd = fd;
this.mClientAddress = clientAddress;
this.mServerAddress = serverAddress;
this.mPort = port;
mCountDownLatch = new CountDownLatch(1);
}
private ConnectionRequest() {
mClientFd = null;
mClientAddress = null;
mServerAddress = null;
mPort = -1;
mCountDownLatch = new CountDownLatch(0);
}
}
private static byte[] initialConnectionInfo(String addr, int port) {
byte[] mac = MacAddressGenerator.convertStringMacAddress(addr);
int channel = port;
int status = 0;
if (VERSION.SDK_INT < 23) {
byte[] signal = new byte[16];
short signalSize = 16;
ByteBuffer buffer = ByteBuffer.wrap(signal);
buffer.order(ByteOrder.LITTLE_ENDIAN)
.putShort(signalSize)
.put(mac)
.putInt(channel)
.putInt(status);
return buffer.array();
} else {
byte[] signal = new byte[20];
short signalSize = 20;
short maxTxPacketSize = 10000;
short maxRxPacketSize = 10000;
ByteBuffer buffer = ByteBuffer.wrap(signal);
buffer.order(ByteOrder.LITTLE_ENDIAN)
.putShort(signalSize)
.put(mac)
.putInt(channel)
.putInt(status)
.putShort(maxTxPacketSize)
.putShort(maxRxPacketSize);
return buffer.array();
}
}
}