blob: 04bef875f804ae186296b97b9078225743b8e28d [file] [log] [blame]
Pavel Maltsev6b49b9b2019-03-14 10:14:24 -07001/*
2 * Copyright (C) 2019 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
17package android.car.usb.handler;
18
19import static android.car.AoapService.KEY_DEVICE;
20import static android.car.AoapService.KEY_RESULT;
21import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP;
22import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP_RESPONSE;
23import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED;
24import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED_RESPONSE;
25
26import android.annotation.Nullable;
27import android.annotation.WorkerThread;
28import android.car.AoapService;
29import android.content.ComponentName;
30import android.content.Context;
31import android.content.Intent;
32import android.content.ServiceConnection;
33import android.hardware.usb.UsbDevice;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.IBinder;
38import android.os.Looper;
39import android.os.Message;
40import android.os.Messenger;
41import android.os.RemoteException;
42import android.util.Log;
43import android.util.SparseArray;
44
45import java.lang.ref.WeakReference;
46import java.util.HashMap;
47import java.util.concurrent.CompletableFuture;
48import java.util.concurrent.ExecutionException;
49import java.util.concurrent.TimeUnit;
50import java.util.concurrent.TimeoutException;
51
52/** Manages connections to {@link android.car.AoapService} (AOAP handler apps). */
53public class AoapServiceManager {
54 private static final String TAG = AoapServiceManager.class.getSimpleName();
55
56 private static final int MSG_DISCONNECT = 1;
57 private static final int DISCONNECT_DELAY_MS = 30000;
58
59 private static final int INVOCATION_TIMEOUT_MS = 5000;
60
61
62 private final HashMap<ComponentName, AoapServiceConnection> mConnections = new HashMap<>();
63 private Context mContext;
64 private final Object mLock = new Object();
65 private final HandlerThread mHandlerThread;
66 private final Handler mHandler;
67
68 public AoapServiceManager(Context context) {
69 mContext = context;
70
71 mHandlerThread = new HandlerThread(TAG);
72 mHandlerThread.start();
73
74 mHandler = new Handler(mHandlerThread.getLooper()) {
75 @Override
76 public void handleMessage(Message msg) {
77 if (msg.what == MSG_DISCONNECT) {
78 removeConnection((AoapServiceConnection) msg.obj);
79 } else {
80 Log.e(TAG, "Unexpected message " + msg.what);
81 }
82 }
83 };
84 }
85
86 /**
87 * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
88 * if it supports the device.
89 */
90 @WorkerThread
91 public boolean isDeviceSupported(UsbDevice device, ComponentName serviceName) {
92 final AoapServiceConnection connection = getConnectionOrNull(serviceName);
93 if (connection == null) {
94 return false;
95 }
96
97 try {
98 return connection.isDeviceSupported(device)
99 .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
100 } catch (ExecutionException | InterruptedException | TimeoutException e) {
101 Log.w(TAG, "Failed to get response isDeviceSupported from " + serviceName, e);
102 return false;
103 }
104 }
105
106 /**
107 * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
108 * if the device can be switched to AOAP mode now.
109 */
110 @WorkerThread
111 public boolean canSwitchDeviceToAoap(UsbDevice device, ComponentName serviceName) {
112 final AoapServiceConnection connection = getConnectionOrNull(serviceName);
113 if (connection == null) {
114 return false;
115 }
116
117 try {
118 return connection.canSwitchDeviceToAoap(device)
119 .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
120 } catch (ExecutionException | InterruptedException | TimeoutException e) {
121 Log.w(TAG, "Failed to get response of canSwitchDeviceToAoap from " + serviceName, e);
122 return false;
123 }
124 }
125
126 @Nullable
127 private AoapServiceConnection getConnectionOrNull(ComponentName name) {
128 AoapServiceConnection connection;
129 synchronized (mLock) {
130 connection = mConnections.get(name);
131 if (connection != null) {
132 postponeServiceDisconnection(connection);
133 return connection;
134 }
135
136 connection = new AoapServiceConnection(name, this, mHandlerThread.getLooper());
137 boolean bound = mContext.bindService(
138 createIntent(name), connection, Context.BIND_AUTO_CREATE);
139 if (bound) {
140 mConnections.put(name, connection);
141 postponeServiceDisconnection(connection);
142 } else {
143 Log.w(TAG, "Failed to bind to service " + name);
144 return null;
145 }
146 }
147 return connection;
148 }
149
150 private void postponeServiceDisconnection(AoapServiceConnection connection) {
151 mHandler.removeMessages(MSG_DISCONNECT, connection);
152 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISCONNECT, connection),
153 DISCONNECT_DELAY_MS);
154 }
155
156 private static Intent createIntent(ComponentName name) {
157 Intent intent = new Intent();
158 intent.setComponent(name);
159 return intent;
160 }
161
162 private void removeConnection(AoapServiceConnection connection) {
163 Log.i(TAG, "Removing connection to " + connection);
164 synchronized (mLock) {
165 mConnections.remove(connection.mComponentName);
166 if (connection.mBound) {
167 mContext.unbindService(connection);
168 connection.mBound = false;
169 }
170 }
171 }
172
173 private static class AoapServiceConnection implements ServiceConnection {
174 private Messenger mOutgoingMessenger;
175 private boolean mBound;
176 private final CompletableFuture<Void> mConnected = new CompletableFuture<>();
177 private final SparseArray<CompletableFuture<Bundle>> mExpectedResponses =
178 new SparseArray<>();
179 private final ComponentName mComponentName;
180 private final WeakReference<AoapServiceManager> mManagerRef;
181 private final Messenger mIncomingMessenger;
182 private final Object mLock = new Object();
183
184 private AoapServiceConnection(ComponentName name, AoapServiceManager manager,
185 Looper looper) {
186 mComponentName = name;
187 mManagerRef = new WeakReference<>(manager);
188 mIncomingMessenger = new Messenger(new Handler(looper) {
189 @Override
190 public void handleMessage(Message msg) {
191 onResponse(msg);
192 }
193 });
194 }
195
196 @Override
197 public void onServiceConnected(ComponentName name, IBinder service) {
198 if (service == null) {
199 Log.e(TAG, "Binder object was not provided on service connection to " + name);
200 return;
201 }
202
203 synchronized (mLock) {
204 mBound = true;
205 mOutgoingMessenger = new Messenger(service);
206 }
207 mConnected.complete(null);
208 }
209
210 @Override
211 public void onServiceDisconnected(ComponentName name) {
212 synchronized (mLock) {
213 mOutgoingMessenger = null;
214 mBound = false;
215 }
216
217 final AoapServiceManager mgr = mManagerRef.get();
218 if (mgr != null) {
219 mgr.removeConnection(this);
220 }
221 }
222
223 private void onResponse(Message message) {
224 final CompletableFuture<Bundle> response;
225 synchronized (mLock) {
226 response = mExpectedResponses.removeReturnOld(message.what);
227 }
228 if (response == null) {
229 Log.e(TAG, "Received unexpected response " + message.what + ", expected: "
230 + mExpectedResponses);
231 return;
232 }
233
234 if (message.getData() == null) {
235 throw new IllegalArgumentException("Received response msg " + message.what
236 + " without data");
237 }
238 Log.i(TAG, "onResponse msg: " + message.what + ", data: " + message.getData());
239 boolean res = response.complete(message.getData());
240 if (!res) {
241 Log.w(TAG, "Failed to complete future " + response);
242 }
243 }
244
245 CompletableFuture<Boolean> isDeviceSupported(UsbDevice device) {
246 return sendMessageForResult(
247 MSG_NEW_DEVICE_ATTACHED,
248 MSG_NEW_DEVICE_ATTACHED_RESPONSE,
249 createUsbDeviceData(device))
250 .thenApply(this::isResultOk);
251
252 }
253
254 CompletableFuture<Boolean> canSwitchDeviceToAoap(UsbDevice device) {
255 return sendMessageForResult(
256 MSG_CAN_SWITCH_TO_AOAP,
257 MSG_CAN_SWITCH_TO_AOAP_RESPONSE,
258 createUsbDeviceData(device))
259 .thenApply(this::isResultOk);
260 }
261
262 private boolean isResultOk(Bundle data) {
263 int result = data.getInt(KEY_RESULT);
264 Log.i(TAG, "Got result: " + data);
265 return AoapService.RESULT_OK == result;
266 }
267
268 private static Bundle createUsbDeviceData(UsbDevice device) {
269 Bundle data = new Bundle(1);
270 data.putParcelable(KEY_DEVICE, device);
271 return data;
272 }
273
274 private CompletableFuture<Bundle> sendMessageForResult(
275 int msgRequest, int msgResponse, Bundle data) {
276 return mConnected.thenCompose(x -> {
277 CompletableFuture<Bundle> responseFuture = new CompletableFuture<>();
278 Messenger messenger;
279 synchronized (mLock) {
280 mExpectedResponses.put(msgResponse, responseFuture);
281 messenger = mOutgoingMessenger;
282 }
283 send(messenger, msgRequest, data);
284
285 return responseFuture;
286 });
287 }
288
289 private void send(Messenger messenger, int req, Bundle data) {
290 Message msg = Message.obtain(null, req, null);
291 msg.replyTo = mIncomingMessenger;
292 msg.setData(data);
293 try {
294 messenger.send(msg);
295 } catch (RemoteException e) {
296 throw new RuntimeException("Connection broken with " + mComponentName, e);
297 }
298 }
299
300 @Override
301 public String toString() {
302 return "AoapServiceConnection{"
303 + "mBound=" + mBound
304 + ", mConnected=" + mConnected
305 + ", mExpectedResponses=" + mExpectedResponses
306 + ", mComponentName=" + mComponentName
307 + '}';
308 }
309 }
310}