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 com.android.internal.annotations.VisibleForTesting; 20 import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl; 21 import com.android.libraries.testing.deviceshadower.internal.utils.Logger; 22 23 import com.google.common.collect.Sets; 24 25 import java.io.FileDescriptor; 26 import java.util.HashMap; 27 import java.util.Map; 28 import java.util.Map.Entry; 29 import java.util.Set; 30 import java.util.concurrent.BlockingQueue; 31 import java.util.concurrent.ConcurrentHashMap; 32 import java.util.concurrent.LinkedBlockingQueue; 33 import java.util.concurrent.atomic.AtomicBoolean; 34 35 /** 36 * A class represents a physical link for communications between two Bluetooth devices. 37 */ 38 public class PhysicalLink { 39 40 // Intended to use RfcommDelegate 41 private static final Logger LOGGER = Logger.create("RfcommDelegate"); 42 43 private final Object mLock; 44 // Every socket has unique FileDescriptor, so use it as socket identifier during communication 45 private final Map<FileDescriptor, RfcommSocketConnection> mConnectionLookup; 46 // Map fd of a socket to the fd of the other socket it connects to 47 private final Map<FileDescriptor, FileDescriptor> mFdMap; 48 private final Set<RfcommSocketConnection> mConnections; 49 private final AtomicBoolean mIsEncrypted; 50 private final Map<String, RfcommDelegate.Callback> mCallbacks = new HashMap<>(); 51 PhysicalLink(String address1, String address2)52 public PhysicalLink(String address1, String address2) { 53 this(address1, 54 DeviceShadowEnvironmentImpl.getBlueletImpl(address1).getRfcommDelegate().mCallback, 55 address2, 56 DeviceShadowEnvironmentImpl.getBlueletImpl(address2).getRfcommDelegate().mCallback, 57 new ConcurrentHashMap<FileDescriptor, RfcommSocketConnection>(), 58 new ConcurrentHashMap<FileDescriptor, FileDescriptor>(), 59 Sets.<RfcommSocketConnection>newConcurrentHashSet()); 60 } 61 62 @VisibleForTesting PhysicalLink(String address1, RfcommDelegate.Callback callback1, String address2, RfcommDelegate.Callback callback2, Map<FileDescriptor, RfcommSocketConnection> connectionLookup, Map<FileDescriptor, FileDescriptor> fdMap, Set<RfcommSocketConnection> connections)63 PhysicalLink(String address1, RfcommDelegate.Callback callback1, 64 String address2, RfcommDelegate.Callback callback2, 65 Map<FileDescriptor, RfcommSocketConnection> connectionLookup, 66 Map<FileDescriptor, FileDescriptor> fdMap, 67 Set<RfcommSocketConnection> connections) { 68 mLock = new Object(); 69 mCallbacks.put(address1, callback1); 70 mCallbacks.put(address2, callback2); 71 this.mConnectionLookup = connectionLookup; 72 this.mFdMap = fdMap; 73 this.mConnections = connections; 74 mIsEncrypted = new AtomicBoolean(false); 75 } 76 addConnection(FileDescriptor fd1, FileDescriptor fd2)77 public void addConnection(FileDescriptor fd1, FileDescriptor fd2) { 78 synchronized (mLock) { 79 int oldSize = mConnections.size(); 80 RfcommSocketConnection connection = new RfcommSocketConnection( 81 FileDescriptorFactory.getInstance().getAddress(fd1), 82 FileDescriptorFactory.getInstance().getAddress(fd2) 83 ); 84 mConnections.add(connection); 85 mConnectionLookup.put(fd1, connection); 86 mConnectionLookup.put(fd2, connection); 87 mFdMap.put(fd1, fd2); 88 mFdMap.put(fd2, fd1); 89 if (oldSize == 0) { 90 onConnectionStateChange(true); 91 } 92 } 93 } 94 95 // TODO(b/79994182): see go/objecttostring-lsc 96 @SuppressWarnings("ObjectToString") closeConnection(FileDescriptor fd)97 public void closeConnection(FileDescriptor fd) { 98 // check for early return without locking 99 if (!mConnectionLookup.containsKey(fd)) { 100 // TODO(b/79994182): FileDescriptor does not implement toString() in fd 101 LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd); 102 return; 103 } 104 synchronized (mLock) { 105 RfcommSocketConnection connection = mConnectionLookup.get(fd); 106 if (connection == null) { 107 // TODO(b/79994182): FileDescriptor does not implement toString() in fd 108 LOGGER.d("Connection doesn't exist, FileDescriptor: " + fd); 109 return; 110 } 111 int oldSize = mConnections.size(); 112 FileDescriptor connectingFd = mFdMap.get(fd); 113 mConnectionLookup.remove(fd); 114 mConnectionLookup.remove(connectingFd); 115 mFdMap.remove(fd); 116 mFdMap.remove(connectingFd); 117 mConnections.remove(connection); 118 if (oldSize == 1) { 119 onConnectionStateChange(false); 120 } 121 } 122 } 123 getConnection(FileDescriptor fd)124 public RfcommSocketConnection getConnection(FileDescriptor fd) { 125 return mConnectionLookup.get(fd); 126 } 127 encrypt()128 public void encrypt() { 129 mIsEncrypted.set(true); 130 } 131 isEncrypted()132 public boolean isEncrypted() { 133 return mIsEncrypted.get(); 134 } 135 isConnected()136 public boolean isConnected() { 137 return !mConnections.isEmpty(); 138 } 139 onConnectionStateChange(boolean isConnected)140 private void onConnectionStateChange(boolean isConnected) { 141 for (Entry<String, RfcommDelegate.Callback> entry : mCallbacks.entrySet()) { 142 RfcommDelegate.Callback callback = entry.getValue(); 143 String localAddress = entry.getKey(); 144 callback.onConnectionStateChange(getRemoteAddress(localAddress), isConnected); 145 } 146 } 147 getRemoteAddress(String address)148 private String getRemoteAddress(String address) { 149 String remoteAddress = null; 150 for (String addr : mCallbacks.keySet()) { 151 if (!addr.equals(address)) { 152 remoteAddress = addr; 153 break; 154 } 155 } 156 return remoteAddress; 157 } 158 159 /** 160 * Represents a Rfcomm socket connection between two {@link android.bluetooth.BluetoothSocket}. 161 */ 162 public static class RfcommSocketConnection { 163 164 final Map<String, BlockingQueue<Integer>> mIncomingDataMap; // address : incomingData 165 RfcommSocketConnection(String address1, String address2)166 public RfcommSocketConnection(String address1, String address2) { 167 mIncomingDataMap = new ConcurrentHashMap<>(); 168 mIncomingDataMap.put(address1, new LinkedBlockingQueue<Integer>()); 169 mIncomingDataMap.put(address2, new LinkedBlockingQueue<Integer>()); 170 } 171 write(String address, int b)172 public void write(String address, int b) throws InterruptedException { 173 mIncomingDataMap.get(address).put(b); 174 } 175 read(String address)176 public int read(String address) throws InterruptedException { 177 return mIncomingDataMap.get(address).take(); 178 } 179 } 180 } 181