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