blob: e474c696526e989299aa515bc67f57100157bef2 [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 com.android.internal.annotations.VisibleForTesting;
import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
import com.google.common.collect.Sets;
import java.io.FileDescriptor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A class represents a physical link for communications between two Bluetooth devices.
*/
public class PhysicalLink {
// Intended to use RfcommDelegate
private static final Logger LOGGER = Logger.create("RfcommDelegate");
private final Object mLock;
// Every socket has unique FileDescriptor, so use it as socket identifier during communication
private final Map<FileDescriptor, RfcommSocketConnection> mConnectionLookup;
// Map fd of a socket to the fd of the other socket it connects to
private final Map<FileDescriptor, FileDescriptor> mFdMap;
private final Set<RfcommSocketConnection> mConnections;
private final AtomicBoolean mIsEncrypted;
private final Map<String, RfcommDelegate.Callback> mCallbacks = new HashMap<>();
public PhysicalLink(String address1, String address2) {
this(address1,
DeviceShadowEnvironmentImpl.getBlueletImpl(address1).getRfcommDelegate().mCallback,
address2,
DeviceShadowEnvironmentImpl.getBlueletImpl(address2).getRfcommDelegate().mCallback,
new ConcurrentHashMap<FileDescriptor, RfcommSocketConnection>(),
new ConcurrentHashMap<FileDescriptor, FileDescriptor>(),
Sets.<RfcommSocketConnection>newConcurrentHashSet());
}
@VisibleForTesting
PhysicalLink(String address1, RfcommDelegate.Callback callback1,
String address2, RfcommDelegate.Callback callback2,
Map<FileDescriptor, RfcommSocketConnection> connectionLookup,
Map<FileDescriptor, FileDescriptor> fdMap,
Set<RfcommSocketConnection> connections) {
mLock = new Object();
mCallbacks.put(address1, callback1);
mCallbacks.put(address2, callback2);
this.mConnectionLookup = connectionLookup;
this.mFdMap = fdMap;
this.mConnections = connections;
mIsEncrypted = new AtomicBoolean(false);
}
public void addConnection(FileDescriptor fd1, FileDescriptor fd2) {
synchronized (mLock) {
int oldSize = mConnections.size();
RfcommSocketConnection connection = new RfcommSocketConnection(
FileDescriptorFactory.getInstance().getAddress(fd1),
FileDescriptorFactory.getInstance().getAddress(fd2)
);
mConnections.add(connection);
mConnectionLookup.put(fd1, connection);
mConnectionLookup.put(fd2, connection);
mFdMap.put(fd1, fd2);
mFdMap.put(fd2, fd1);
if (oldSize == 0) {
onConnectionStateChange(true);
}
}
}
// TODO(b/79994182): see go/objecttostring-lsc
@SuppressWarnings("ObjectToString")
public void closeConnection(FileDescriptor fd) {
// check for early return without locking
if (!mConnectionLookup.containsKey(fd)) {
// TODO(b/79994182): FileDescriptor does not implement toString() in fd
LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
return;
}
synchronized (mLock) {
RfcommSocketConnection connection = mConnectionLookup.get(fd);
if (connection == null) {
// TODO(b/79994182): FileDescriptor does not implement toString() in fd
LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd);
return;
}
int oldSize = mConnections.size();
FileDescriptor connectingFd = mFdMap.get(fd);
mConnectionLookup.remove(fd);
mConnectionLookup.remove(connectingFd);
mFdMap.remove(fd);
mFdMap.remove(connectingFd);
mConnections.remove(connection);
if (oldSize == 1) {
onConnectionStateChange(false);
}
}
}
public RfcommSocketConnection getConnection(FileDescriptor fd) {
return mConnectionLookup.get(fd);
}
public void encrypt() {
mIsEncrypted.set(true);
}
public boolean isEncrypted() {
return mIsEncrypted.get();
}
public boolean isConnected() {
return !mConnections.isEmpty();
}
private void onConnectionStateChange(boolean isConnected) {
for (Entry<String, RfcommDelegate.Callback> entry : mCallbacks.entrySet()) {
RfcommDelegate.Callback callback = entry.getValue();
String localAddress = entry.getKey();
callback.onConnectionStateChange(getRemoteAddress(localAddress), isConnected);
}
}
private String getRemoteAddress(String address) {
String remoteAddress = null;
for (String addr : mCallbacks.keySet()) {
if (!addr.equals(address)) {
remoteAddress = addr;
break;
}
}
return remoteAddress;
}
/**
* Represents a Rfcomm socket connection between two {@link android.bluetooth.BluetoothSocket}.
*/
public static class RfcommSocketConnection {
final Map<String, BlockingQueue<Integer>> mIncomingDataMap; // address : incomingData
public RfcommSocketConnection(String address1, String address2) {
mIncomingDataMap = new ConcurrentHashMap<>();
mIncomingDataMap.put(address1, new LinkedBlockingQueue<Integer>());
mIncomingDataMap.put(address2, new LinkedBlockingQueue<Integer>());
}
public void write(String address, int b) throws InterruptedException {
mIncomingDataMap.get(address).put(b);
}
public int read(String address) throws InterruptedException {
return mIncomingDataMap.get(address).take();
}
}
}