blob: f0656aa75ef17819ca2ae7075c57b6893602ac27 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.car.usb.handler;
import static android.car.AoapService.KEY_DEVICE;
import static android.car.AoapService.KEY_RESULT;
import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP;
import static android.car.AoapService.MSG_CAN_SWITCH_TO_AOAP_RESPONSE;
import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED;
import static android.car.AoapService.MSG_NEW_DEVICE_ATTACHED_RESPONSE;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.car.AoapService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Manages connections to {@link android.car.AoapService} (AOAP handler apps). */
public class AoapServiceManager {
private static final String TAG = AoapServiceManager.class.getSimpleName();
private static final int MSG_DISCONNECT = 1;
private static final int DISCONNECT_DELAY_MS = 30000;
private static final int INVOCATION_TIMEOUT_MS = 20000;
private final HashMap<ComponentName, AoapServiceConnection> mConnections = new HashMap<>();
private Context mContext;
private final Object mLock = new Object();
private final HandlerThread mHandlerThread;
private final Handler mHandler;
public AoapServiceManager(Context context) {
mContext = context;
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_DISCONNECT) {
removeConnection((AoapServiceConnection) msg.obj);
} else {
Log.e(TAG, "Unexpected message " + msg.what);
}
}
};
}
/**
* Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
* if it supports the device.
*/
@WorkerThread
public boolean isDeviceSupported(UsbDevice device, ComponentName serviceName) {
final AoapServiceConnection connection = getConnectionOrNull(serviceName);
if (connection == null) {
return false;
}
try {
return connection.isDeviceSupported(device)
.get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, "Failed to get response isDeviceSupported from " + serviceName, e);
return false;
}
}
/**
* Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
* if the device can be switched to AOAP mode now.
*/
@WorkerThread
public boolean canSwitchDeviceToAoap(UsbDevice device, ComponentName serviceName) {
final AoapServiceConnection connection = getConnectionOrNull(serviceName);
if (connection == null) {
return false;
}
try {
return connection.canSwitchDeviceToAoap(device)
.get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, "Failed to get response of canSwitchDeviceToAoap from " + serviceName, e);
return false;
}
}
@Nullable
private AoapServiceConnection getConnectionOrNull(ComponentName name) {
AoapServiceConnection connection;
synchronized (mLock) {
connection = mConnections.get(name);
if (connection != null) {
postponeServiceDisconnection(connection);
return connection;
}
connection = new AoapServiceConnection(name, this, mHandlerThread.getLooper());
boolean bound = mContext.bindService(
createIntent(name), connection, Context.BIND_AUTO_CREATE);
if (bound) {
mConnections.put(name, connection);
postponeServiceDisconnection(connection);
} else {
Log.w(TAG, "Failed to bind to service " + name);
return null;
}
}
return connection;
}
private void postponeServiceDisconnection(AoapServiceConnection connection) {
mHandler.removeMessages(MSG_DISCONNECT, connection);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISCONNECT, connection),
DISCONNECT_DELAY_MS);
}
private static Intent createIntent(ComponentName name) {
Intent intent = new Intent();
intent.setComponent(name);
return intent;
}
private void removeConnection(AoapServiceConnection connection) {
Log.i(TAG, "Removing connection to " + connection);
synchronized (mLock) {
mConnections.remove(connection.mComponentName);
if (connection.mBound) {
mContext.unbindService(connection);
connection.mBound = false;
}
}
}
private static class AoapServiceConnection implements ServiceConnection {
private Messenger mOutgoingMessenger;
private boolean mBound;
private final CompletableFuture<Void> mConnected = new CompletableFuture<>();
private final SparseArray<CompletableFuture<Bundle>> mExpectedResponses =
new SparseArray<>();
private final ComponentName mComponentName;
private final WeakReference<AoapServiceManager> mManagerRef;
private final Messenger mIncomingMessenger;
private final Object mLock = new Object();
private AoapServiceConnection(ComponentName name, AoapServiceManager manager,
Looper looper) {
mComponentName = name;
mManagerRef = new WeakReference<>(manager);
mIncomingMessenger = new Messenger(new Handler(looper) {
@Override
public void handleMessage(Message msg) {
onResponse(msg);
}
});
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (service == null) {
Log.e(TAG, "Binder object was not provided on service connection to " + name);
return;
}
synchronized (mLock) {
mBound = true;
mOutgoingMessenger = new Messenger(service);
}
mConnected.complete(null);
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
mOutgoingMessenger = null;
mBound = false;
}
final AoapServiceManager mgr = mManagerRef.get();
if (mgr != null) {
mgr.removeConnection(this);
}
}
private void onResponse(Message message) {
final CompletableFuture<Bundle> response;
synchronized (mLock) {
response = mExpectedResponses.removeReturnOld(message.what);
}
if (response == null) {
Log.e(TAG, "Received unexpected response " + message.what + ", expected: "
+ mExpectedResponses);
return;
}
if (message.getData() == null) {
throw new IllegalArgumentException("Received response msg " + message.what
+ " without data");
}
Log.i(TAG, "onResponse msg: " + message.what + ", data: " + message.getData());
boolean res = response.complete(message.getData());
if (!res) {
Log.w(TAG, "Failed to complete future " + response);
}
}
CompletableFuture<Boolean> isDeviceSupported(UsbDevice device) {
return sendMessageForResult(
MSG_NEW_DEVICE_ATTACHED,
MSG_NEW_DEVICE_ATTACHED_RESPONSE,
createUsbDeviceData(device))
.thenApply(this::isResultOk);
}
CompletableFuture<Boolean> canSwitchDeviceToAoap(UsbDevice device) {
return sendMessageForResult(
MSG_CAN_SWITCH_TO_AOAP,
MSG_CAN_SWITCH_TO_AOAP_RESPONSE,
createUsbDeviceData(device))
.thenApply(this::isResultOk);
}
private boolean isResultOk(Bundle data) {
int result = data.getInt(KEY_RESULT);
Log.i(TAG, "Got result: " + data);
return AoapService.RESULT_OK == result;
}
private static Bundle createUsbDeviceData(UsbDevice device) {
Bundle data = new Bundle(1);
data.putParcelable(KEY_DEVICE, device);
return data;
}
private CompletableFuture<Bundle> sendMessageForResult(
int msgRequest, int msgResponse, Bundle data) {
return mConnected.thenCompose(x -> {
CompletableFuture<Bundle> responseFuture = new CompletableFuture<>();
Messenger messenger;
synchronized (mLock) {
mExpectedResponses.put(msgResponse, responseFuture);
messenger = mOutgoingMessenger;
}
send(messenger, msgRequest, data);
return responseFuture;
});
}
private void send(Messenger messenger, int req, Bundle data) {
Message msg = Message.obtain(null, req, null);
msg.replyTo = mIncomingMessenger;
msg.setData(data);
try {
messenger.send(msg);
} catch (RemoteException e) {
throw new RuntimeException("Connection broken with " + mComponentName, e);
}
}
@Override
public String toString() {
return "AoapServiceConnection{"
+ "mBound=" + mBound
+ ", mConnected=" + mConnected
+ ", mExpectedResponses=" + mExpectedResponses
+ ", mComponentName=" + mComponentName
+ '}';
}
}
}