blob: 0da71ac5f13851c8e4c5b039afe934f29a2cbdea [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Controller used to handle USB device connections.
* TODO: Support handling multiple new USB devices at the same time.
*/
public final class UsbHostController
implements UsbDeviceHandlerResolver.UsbDeviceHandlerResolverCallback {
/**
* Callbacks for controller
*/
public interface UsbHostControllerCallbacks {
/** Host controller ready for shutdown */
void shutdown();
/** Change of processing state */
void processingStarted();
/** Title of processing changed */
void titleChanged(String title);
/** Options for USB device changed */
void optionsUpdated(List<UsbDeviceSettings> options);
}
private static final String TAG = UsbHostController.class.getSimpleName();
private static final boolean LOCAL_LOGD = true;
private static final boolean LOCAL_LOGV = true;
private static final int DISPATCH_RETRY_DELAY_MS = 1000;
private static final int DISPATCH_RETRY_ATTEMPTS = 5;
private final List<UsbDeviceSettings> mEmptyList = new ArrayList<>();
private final Context mContext;
private final UsbHostControllerCallbacks mCallback;
private final UsbSettingsStorage mUsbSettingsStorage;
private final UsbManager mUsbManager;
private final UsbDeviceHandlerResolver mUsbResolver;
private final UsbHostControllerHandler mHandler;
private final BroadcastReceiver mUsbBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
unsetActiveDeviceIfMatch(device);
} else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
setActiveDeviceIfMatch(device);
}
}
};
@GuardedBy("this")
private UsbDevice mActiveDevice;
public UsbHostController(Context context, UsbHostControllerCallbacks callbacks) {
mContext = context;
mCallback = callbacks;
mHandler = new UsbHostControllerHandler(Looper.myLooper());
mUsbSettingsStorage = new UsbSettingsStorage(context);
mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
mUsbResolver = new UsbDeviceHandlerResolver(mUsbManager, mContext, this);
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
context.registerReceiver(mUsbBroadcastReceiver, filter);
}
private synchronized void setActiveDeviceIfMatch(UsbDevice device) {
if (mActiveDevice != null && device != null
&& UsbUtil.isDevicesMatching(device, mActiveDevice)) {
mActiveDevice = device;
}
}
private synchronized void unsetActiveDeviceIfMatch(UsbDevice device) {
mHandler.requestDeviceRemoved();
if (mActiveDevice != null && device != null
&& UsbUtil.isDevicesMatching(device, mActiveDevice)) {
mActiveDevice = null;
}
}
private synchronized boolean startDeviceProcessingIfNull(UsbDevice device) {
if (mActiveDevice == null) {
mActiveDevice = device;
return true;
}
return false;
}
private synchronized void stopDeviceProcessing() {
mActiveDevice = null;
}
private synchronized UsbDevice getActiveDevice() {
return mActiveDevice;
}
private boolean deviceMatchedActiveDevice(UsbDevice device) {
UsbDevice activeDevice = getActiveDevice();
return activeDevice != null && UsbUtil.isDevicesMatching(activeDevice, device);
}
private static String generateTitle(Context context, UsbDevice usbDevice) {
String manufacturer = usbDevice.getManufacturerName();
String product = usbDevice.getProductName();
if (manufacturer == null && product == null) {
return context.getString(R.string.usb_unknown_device);
}
if (manufacturer != null && product != null) {
return manufacturer + " " + product;
}
if (manufacturer != null) {
return manufacturer;
}
return product;
}
/**
* Processes device new device.
* <p>
* It will load existing settings or resolve supported handlers.
*/
public void processDevice(UsbDevice device) {
if (!startDeviceProcessingIfNull(device)) {
Log.w(TAG, "Currently, other device is being processed");
}
mCallback.optionsUpdated(mEmptyList);
mCallback.processingStarted();
UsbDeviceSettings settings = mUsbSettingsStorage.getSettings(device);
if (settings == null) {
resolveDevice(device);
} else {
Object obj =
new UsbHostControllerHandlerDispatchData(
device, settings, DISPATCH_RETRY_ATTEMPTS, true);
Message.obtain(mHandler, UsbHostControllerHandler.MSG_DEVICE_DISPATCH, obj)
.sendToTarget();
}
}
/**
* Applies device settings.
*/
public void applyDeviceSettings(UsbDeviceSettings settings) {
mUsbSettingsStorage.saveSettings(settings);
Message msg = mHandler.obtainMessage();
msg.obj =
new UsbHostControllerHandlerDispatchData(
getActiveDevice(), settings, DISPATCH_RETRY_ATTEMPTS, false);
msg.what = UsbHostControllerHandler.MSG_DEVICE_DISPATCH;
msg.sendToTarget();
}
private void resolveDevice(UsbDevice device) {
mCallback.titleChanged(generateTitle(mContext, device));
mUsbResolver.resolve(device);
}
/**
* Release object.
*/
public void release() {
mContext.unregisterReceiver(mUsbBroadcastReceiver);
mUsbResolver.release();
}
private boolean isDeviceAoapPossible(UsbDevice device) {
if (AoapInterface.isDeviceInAoapMode(device)) {
return true;
}
UsbManager usbManager = mContext.getSystemService(UsbManager.class);
UsbDeviceConnection connection = UsbUtil.openConnection(usbManager, device);
boolean aoapSupported = AoapInterface.isSupported(mContext, device, connection);
connection.close();
return aoapSupported;
}
@Override
public void onHandlersResolveCompleted(
UsbDevice device, List<UsbDeviceSettings> handlers) {
if (LOCAL_LOGD) {
Log.d(TAG, "onHandlersResolveComplete: " + device);
}
if (deviceMatchedActiveDevice(device)) {
if (handlers.isEmpty()) {
onDeviceDispatched();
} else if (handlers.size() == 1) {
applyDeviceSettings(handlers.get(0));
} else {
if (isDeviceAoapPossible(device)) {
// Device supports AOAP mode, if we have just single AOAP handler then use it
// instead of showing disambiguation dialog to the user.
UsbDeviceSettings aoapHandler = getSingleAoapDeviceHandlerOrNull(handlers);
if (aoapHandler != null) {
applyDeviceSettings(aoapHandler);
return;
}
}
mCallback.optionsUpdated(handlers);
}
} else {
Log.w(TAG, "Handlers ignored as they came for inactive device");
}
}
private UsbDeviceSettings getSingleAoapDeviceHandlerOrNull(List<UsbDeviceSettings> handlers) {
UsbDeviceSettings aoapHandler = null;
for (UsbDeviceSettings handler : handlers) {
if (handler.getAoap()) {
if (aoapHandler != null) { // Found multiple AOAP handlers.
return null;
}
aoapHandler = handler;
}
}
return aoapHandler;
}
@Override
public void onDeviceDispatched() {
stopDeviceProcessing();
mCallback.shutdown();
}
void doHandleDeviceRemoved() {
if (getActiveDevice() == null) {
if (LOCAL_LOGD) {
Log.d(TAG, "USB device detached");
}
stopDeviceProcessing();
mCallback.shutdown();
}
}
private class UsbHostControllerHandlerDispatchData {
private final UsbDevice mUsbDevice;
private final UsbDeviceSettings mUsbDeviceSettings;
public int mRetries = 0;
public boolean mCanResolve = true;
public UsbHostControllerHandlerDispatchData(
UsbDevice usbDevice, UsbDeviceSettings usbDeviceSettings,
int retries, boolean canResolve) {
mUsbDevice = usbDevice;
mUsbDeviceSettings = usbDeviceSettings;
mRetries = retries;
mCanResolve = canResolve;
}
public UsbDevice getUsbDevice() {
return mUsbDevice;
}
public UsbDeviceSettings getUsbDeviceSettings() {
return mUsbDeviceSettings;
}
}
private class UsbHostControllerHandler extends Handler {
private static final int MSG_DEVICE_REMOVED = 1;
private static final int MSG_DEVICE_DISPATCH = 2;
private static final int DEVICE_REMOVE_TIMEOUT_MS = 500;
// Used to get the device that we are trying to connect to, if mActiveDevice is removed and
// startAoap fails afterwards. Used during USB enumeration when retrying to startAoap when
// there are multiple devices attached.
private int mLastDeviceId = 0;
private int mStartAoapRetries = 1;
private UsbHostControllerHandler(Looper looper) {
super(looper);
}
private void requestDeviceRemoved() {
sendEmptyMessageDelayed(MSG_DEVICE_REMOVED, DEVICE_REMOVE_TIMEOUT_MS);
}
private void onFailure() {
if (mStartAoapRetries == 0) {
Log.w(TAG, "Reached maximum retry count for startAoap. Giving up Aoa handshake.");
return;
}
mStartAoapRetries--;
Log.d(TAG, "Restarting USB enumeration.");
Iterator<UsbDevice> deviceIterator = mUsbManager.getDeviceList().values().iterator();
while (deviceIterator.hasNext()) {
UsbDevice device = deviceIterator.next();
if (mLastDeviceId == device.getDeviceId()) {
processDevice(device);
return;
}
}
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DEVICE_REMOVED:
doHandleDeviceRemoved();
break;
case MSG_DEVICE_DISPATCH:
UsbHostControllerHandlerDispatchData data =
(UsbHostControllerHandlerDispatchData) msg.obj;
UsbDevice device = data.getUsbDevice();
mLastDeviceId = device.getDeviceId();
UsbDeviceSettings settings = data.getUsbDeviceSettings();
if (!mUsbResolver.dispatch(device, settings.getHandler(), settings.getAoap(),
this::onFailure)) {
if (data.mRetries > 0) {
--data.mRetries;
Message nextMessage = Message.obtain(msg);
mHandler.sendMessageDelayed(nextMessage, DISPATCH_RETRY_DELAY_MS);
} else if (data.mCanResolve) {
resolveDevice(device);
}
} else if (LOCAL_LOGV) {
Log.v(TAG, "Usb Device: " + data.getUsbDevice() + " was sent to component: "
+ settings.getHandler());
}
break;
default:
Log.w(TAG, "Unhandled message: " + msg);
super.handleMessage(msg);
}
}
}
}