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