1 /* 2 * Copyright (C) 2024 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.server.ranging.oob; 18 19 import android.annotation.IntDef; 20 import android.app.AlarmManager; 21 import android.os.RemoteException; 22 import android.os.SystemClock; 23 import android.ranging.oob.IOobSendDataListener; 24 import android.ranging.oob.OobHandle; 25 import android.util.Log; 26 import android.util.Pair; 27 28 import androidx.annotation.Nullable; 29 30 import com.android.server.ranging.RangingInjector; 31 import com.android.server.ranging.RangingUtils.StateMachine; 32 33 import com.google.common.collect.Queues; 34 import com.google.common.util.concurrent.FluentFuture; 35 import com.google.common.util.concurrent.Futures; 36 import com.google.common.util.concurrent.SettableFuture; 37 38 import java.lang.annotation.ElementType; 39 import java.lang.annotation.Target; 40 import java.util.concurrent.ConcurrentHashMap; 41 import java.util.concurrent.ConcurrentLinkedQueue; 42 import java.util.concurrent.ConcurrentMap; 43 44 public class OobController { 45 private static final String TAG = OobController.class.getSimpleName(); 46 47 private static final int OOB_DISCONNECT_TIMEOUT_MS = 5_000; 48 49 private final RangingInjector mInjector; 50 private final AlarmManager mAlarmManager; 51 private final ConcurrentMap<OobHandle, OobConnection> mConnections; 52 53 private @Nullable IOobSendDataListener mOobDataSender = null; 54 55 public static class ConnectionClosedException extends Exception { 56 @IntDef(value = { 57 Reason.REQUESTED, 58 Reason.TRANSPORT_CLOSED, 59 Reason.TRANSPORT_TIMEOUT, 60 }) 61 @Target({ElementType.TYPE_USE}) 62 public @interface Reason { 63 int REQUESTED = 0; 64 int TRANSPORT_CLOSED = 1; 65 int TRANSPORT_TIMEOUT = 2; 66 } 67 68 private final @Reason int mReason; 69 ConnectionClosedException(@eason int reason)70 public ConnectionClosedException(@Reason int reason) { 71 super("OOB connection closed"); 72 mReason = reason; 73 } 74 getReason()75 public @Reason int getReason() { 76 return mReason; 77 } 78 } 79 80 /** 81 * An OOB connection between the local device and a remote device. Each connection is 82 * uniquely identified by its {@link OobHandle}. 83 */ 84 public class OobConnection implements AutoCloseable { 85 private final OobHandle mHandle; 86 private final String mDisconnectTimeoutAlarmTag; 87 private final ConcurrentLinkedQueue<Pair<byte[], SettableFuture<Void>>> mPendingDataSends; 88 private final ConcurrentLinkedQueue<SettableFuture<byte[]>> mPendingReceivers; 89 private final ConcurrentLinkedQueue<byte[]> mReceivedData; 90 private final StateMachine<State> mStateMachine = new StateMachine<>(State.CONNECTED); 91 92 /** Invariant: Non-null iff {@code mStateMachine.getState() == State.CLOSED} */ 93 private ConnectionClosedException mClosedException = null; 94 OobConnection(OobHandle handle)95 OobConnection(OobHandle handle) { 96 mHandle = handle; 97 mDisconnectTimeoutAlarmTag = "RangingOobConnection" + mHandle + "DisconnectTimeout"; 98 mPendingDataSends = Queues.newConcurrentLinkedQueue(); 99 mPendingReceivers = Queues.newConcurrentLinkedQueue(); 100 mReceivedData = Queues.newConcurrentLinkedQueue(); 101 } 102 sendData(byte[] data)103 public FluentFuture<Void> sendData(byte[] data) { 104 SettableFuture<Void> future = SettableFuture.create(); 105 setDataSendFuture(data, future); 106 return FluentFuture.from(future); 107 } 108 receiveData()109 public FluentFuture<byte[]> receiveData() { 110 if (mStateMachine.getState() == State.CLOSED) { 111 return FluentFuture.from(Futures.immediateFailedFuture(mClosedException)); 112 } 113 114 if (mReceivedData.isEmpty()) { 115 SettableFuture<byte[]> future = SettableFuture.create(); 116 mPendingReceivers.offer(future); 117 return FluentFuture.from(future); 118 } else { 119 return FluentFuture.from(Futures.immediateFuture(mReceivedData.poll())); 120 } 121 } 122 123 @Override close()124 public void close() { 125 close(ConnectionClosedException.Reason.REQUESTED); 126 } 127 close(@onnectionClosedException.Reason int reason)128 private void close(@ConnectionClosedException.Reason int reason) { 129 synchronized (mStateMachine) { 130 if (mStateMachine.getState() == State.CLOSED) return; 131 mStateMachine.setState(State.CLOSED); 132 mClosedException = new ConnectionClosedException(reason); 133 } 134 135 mPendingDataSends.forEach((sender) -> sender.second.setException(mClosedException)); 136 mPendingDataSends.clear(); 137 138 mPendingReceivers.forEach((receiver) -> receiver.setException(mClosedException)); 139 mPendingReceivers.clear(); 140 141 mReceivedData.clear(); 142 mConnections.remove(mHandle); 143 } 144 145 /** 146 * @return true if the connection is connected. A connection in this state may eventually be 147 * re-established. Data sent while disconnected will be queued up for when the connection 148 * re-establishes. 149 */ isConnected()150 public boolean isConnected() { 151 return mStateMachine.getState() == State.CONNECTED; 152 } 153 154 /** 155 * @return true if the connection is closed. A closed connection cannot be reopened. Sending 156 * or receiving data on a closed connection will result in an error. 157 */ isClosed()158 public boolean isClosed() { 159 return mStateMachine.getState() == State.CLOSED; 160 } 161 handleReceiveData(byte[] data)162 private void handleReceiveData(byte[] data) { 163 if (mPendingReceivers.isEmpty()) { 164 mReceivedData.offer(data); 165 } else { 166 mPendingReceivers.poll().set(data); 167 } 168 } 169 handleDisconnect()170 private void handleDisconnect() { 171 synchronized (mStateMachine) { 172 if (mStateMachine.getState() != State.CONNECTED) return; 173 mStateMachine.setState(State.DISCONNECTED); 174 mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, 175 SystemClock.elapsedRealtime() + OOB_DISCONNECT_TIMEOUT_MS, 176 mDisconnectTimeoutAlarmTag, mDisconnectTimeoutListener, 177 mInjector.getAlarmHandler()); 178 } 179 } 180 handleReconnect()181 private void handleReconnect() { 182 synchronized (mStateMachine) { 183 if (mStateMachine.getState() != State.DISCONNECTED) return; 184 mStateMachine.setState(State.CONNECTED); 185 mAlarmManager.cancel(mDisconnectTimeoutListener); 186 } 187 188 while (mStateMachine.getState() == State.CONNECTED && !mPendingDataSends.isEmpty()) { 189 Pair<byte[], SettableFuture<Void>> send = mPendingDataSends.poll(); 190 setDataSendFuture(send.first, send.second); 191 } 192 } 193 setDataSendFuture(byte[] data, SettableFuture<Void> future)194 private void setDataSendFuture(byte[] data, SettableFuture<Void> future) { 195 if (mOobDataSender == null) { 196 future.setException(new IllegalStateException( 197 "Attempted to send oob message with no data sender registered")); 198 return; 199 } 200 switch (mStateMachine.getState()) { 201 case CLOSED: { 202 future.setException(mClosedException); 203 return; 204 } 205 case CONNECTED: { 206 try { 207 mOobDataSender.sendOobData(mHandle, data); 208 future.setFuture(Futures.immediateVoidFuture()); 209 } catch (RemoteException e) { 210 future.setException(e); 211 } 212 return; 213 } 214 case DISCONNECTED: { 215 mPendingDataSends.add(Pair.create(data, future)); 216 return; 217 } 218 } 219 } 220 221 private final AlarmManager.OnAlarmListener mDisconnectTimeoutListener = () -> { 222 if (mStateMachine.getState() != State.DISCONNECTED) return; 223 224 Log.w(TAG, "Oob connection in disconnected state for longer than timeout of " 225 + OOB_DISCONNECT_TIMEOUT_MS + " ms. Closing..."); 226 close(ConnectionClosedException.Reason.TRANSPORT_TIMEOUT); 227 }; 228 } 229 OobController(RangingInjector injector)230 public OobController(RangingInjector injector) { 231 mInjector = injector; 232 mConnections = new ConcurrentHashMap<>(); 233 mAlarmManager = mInjector.getContext().getSystemService(AlarmManager.class); 234 } 235 registerDataSender(IOobSendDataListener oobDataSender)236 public void registerDataSender(IOobSendDataListener oobDataSender) { 237 if (mOobDataSender != null) { 238 Log.w(TAG, "Re-registered oob send data listener"); 239 } 240 mOobDataSender = oobDataSender; 241 } 242 createConnection(OobHandle handle)243 public OobConnection createConnection(OobHandle handle) { 244 OobConnection connection = new OobConnection(handle); 245 mConnections.put(handle, connection); 246 return connection; 247 } 248 handleOobDataReceived(OobHandle oobHandle, byte[] data)249 public void handleOobDataReceived(OobHandle oobHandle, byte[] data) { 250 OobConnection connection = mConnections.get(oobHandle); 251 if (connection == null) { 252 Log.w(TAG, "Received message on unknown connection " + oobHandle + ". Ignoring..."); 253 } else { 254 connection.handleReceiveData(data); 255 } 256 } 257 handleOobDeviceDisconnected(OobHandle oobHandle)258 public void handleOobDeviceDisconnected(OobHandle oobHandle) { 259 OobConnection connection = mConnections.get(oobHandle); 260 if (connection == null) { 261 Log.w(TAG, "Unknown peer disconnected on handle " + oobHandle + ". Ignoring..."); 262 } else { 263 Log.v(TAG, "A peer with an active connection has disconnected on handle " + oobHandle); 264 connection.handleDisconnect(); 265 } 266 } 267 handleOobDeviceReconnected(OobHandle oobHandle)268 public void handleOobDeviceReconnected(OobHandle oobHandle) { 269 OobConnection connection = mConnections.get(oobHandle); 270 if (connection == null) { 271 Log.w(TAG, "Unknown peer reconnected on handle " + oobHandle + ". Ignoring..."); 272 } else { 273 Log.v(TAG, "The peer on handle " + oobHandle + " has reconnected"); 274 connection.handleReconnect(); 275 } 276 } 277 handleOobClosed(OobHandle oobHandle)278 public void handleOobClosed(OobHandle oobHandle) { 279 OobConnection connection = mConnections.remove(oobHandle); 280 if (connection == null) { 281 Log.w(TAG, "Attempted to close unknown oob connection " + oobHandle + ". Ignoring..."); 282 } else { 283 connection.close(ConnectionClosedException.Reason.TRANSPORT_CLOSED); 284 } 285 } 286 287 private enum State { 288 CONNECTED, 289 DISCONNECTED, 290 CLOSED 291 } 292 } 293