• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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