blob: c0eebbea7da9886cc5964402c29a8aa815b1df01 [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.car.IUsbAoapSupportCheckService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.XmlResourceParser;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import com.android.internal.util.XmlUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.xmlpull.v1.XmlPullParser;
/** Resolves supported handlers for USB device. */
public final class UsbDeviceHandlerResolver {
private static final String TAG = UsbDeviceHandlerResolver.class.getSimpleName();
private static final boolean LOCAL_LOGD = true;
/**
* Callbacks for device resolver.
*/
public interface UsbDeviceHandlerResolverCallback {
/** Handlers are resolved */
void onHandlersResolveCompleted(
UsbDevice device, List<UsbDeviceSettings> availableSettings);
/** Device was dispatched */
void onDeviceDispatched();
}
private final UsbManager mUsbManager;
private final PackageManager mPackageManager;
private final UsbDeviceHandlerResolverCallback mDeviceCallback;
private final Context mContext;
private final HandlerThread mHandlerThread;
private final UsbDeviceResolverHandler mHandler;
private class DeviceContext {
public final UsbDevice usbDevice;
public final UsbDeviceConnection connection;
public final UsbDeviceSettings settings;
public final List<UsbDeviceSettings> activeDeviceSettings;
public final Queue<Pair<ResolveInfo, DeviceFilter>> mActiveDeviceOptions =
new LinkedList<>();
private volatile IUsbAoapSupportCheckService mUsbAoapSupportCheckService;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "onServiceConnected: " + className);
mUsbAoapSupportCheckService = IUsbAoapSupportCheckService.Stub.asInterface(service);
mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "onServiceDisconnected: " + className);
mUsbAoapSupportCheckService = null;
mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
}
};
public DeviceContext(UsbDevice usbDevice, UsbDeviceSettings settings,
List<UsbDeviceSettings> activeDeviceSettings) {
this.usbDevice = usbDevice;
this.settings = settings;
this.activeDeviceSettings = activeDeviceSettings;
connection = UsbUtil.openConnection(mUsbManager, usbDevice);
}
}
// This class is used to describe a USB device.
// When used in HashMaps all values must be specified,
// but wildcards can be used for any of the fields in
// the package meta-data.
private static class DeviceFilter {
// USB Vendor ID (or -1 for unspecified)
public final int mVendorId;
// USB Product ID (or -1 for unspecified)
public final int mProductId;
// USB device or interface class (or -1 for unspecified)
public final int mClass;
// USB device subclass (or -1 for unspecified)
public final int mSubclass;
// USB device protocol (or -1 for unspecified)
public final int mProtocol;
// USB device manufacturer name string (or null for unspecified)
public final String mManufacturerName;
// USB device product name string (or null for unspecified)
public final String mProductName;
// USB device serial number string (or null for unspecified)
public final String mSerialNumber;
// USB device in AOAP mode manufacturer
public final String mAoapManufacturer;
// USB device in AOAP mode model
public final String mAoapModel;
// USB device in AOAP mode description string
public final String mAoapDescription;
// USB device in AOAP mode version
public final String mAoapVersion;
// USB device in AOAP mode URI
public final String mAoapUri;
// USB device in AOAP mode serial
public final String mAoapSerial;
// USB device in AOAP mode verification service
public final String mAoapService;
DeviceFilter(int vid, int pid, int clasz, int subclass, int protocol,
String manufacturer, String product, String serialnum,
String aoapManufacturer, String aoapModel, String aoapDescription,
String aoapVersion, String aoapUri, String aoapSerial,
String aoapService) {
mVendorId = vid;
mProductId = pid;
mClass = clasz;
mSubclass = subclass;
mProtocol = protocol;
mManufacturerName = manufacturer;
mProductName = product;
mSerialNumber = serialnum;
mAoapManufacturer = aoapManufacturer;
mAoapModel = aoapModel;
mAoapDescription = aoapDescription;
mAoapVersion = aoapVersion;
mAoapUri = aoapUri;
mAoapSerial = aoapSerial;
mAoapService = aoapService;
}
DeviceFilter(UsbDevice device) {
mVendorId = device.getVendorId();
mProductId = device.getProductId();
mClass = device.getDeviceClass();
mSubclass = device.getDeviceSubclass();
mProtocol = device.getDeviceProtocol();
mManufacturerName = device.getManufacturerName();
mProductName = device.getProductName();
mSerialNumber = device.getSerialNumber();
mAoapManufacturer = null;
mAoapModel = null;
mAoapDescription = null;
mAoapVersion = null;
mAoapUri = null;
mAoapSerial = null;
mAoapService = null;
}
public static DeviceFilter read(XmlPullParser parser, boolean aoapData) {
int vendorId = -1;
int productId = -1;
int deviceClass = -1;
int deviceSubclass = -1;
int deviceProtocol = -1;
String manufacturerName = null;
String productName = null;
String serialNumber = null;
String aoapManufacturer = null;
String aoapModel = null;
String aoapDescription = null;
String aoapVersion = null;
String aoapUri = null;
String aoapSerial = null;
String aoapService = null;
int count = parser.getAttributeCount();
for (int i = 0; i < count; i++) {
String name = parser.getAttributeName(i);
String value = parser.getAttributeValue(i);
// Attribute values are ints or strings
if (!aoapData && "manufacturer-name".equals(name)) {
manufacturerName = value;
} else if (!aoapData && "product-name".equals(name)) {
productName = value;
} else if (!aoapData && "serial-number".equals(name)) {
serialNumber = value;
} else if (aoapData && "manufacturer".equals(name)) {
aoapManufacturer = value;
} else if (aoapData && "model".equals(name)) {
aoapModel = value;
} else if (aoapData && "description".equals(name)) {
aoapDescription = value;
} else if (aoapData && "version".equals(name)) {
aoapVersion = value;
} else if (aoapData && "uri".equals(name)) {
aoapUri = value;
} else if (aoapData && "serial".equals(name)) {
aoapSerial = value;
} else if (aoapData && "service".equals(name)) {
aoapService = value;
} else if (!aoapData) {
int intValue = -1;
int radix = 10;
if (value != null && value.length() > 2 && value.charAt(0) == '0'
&& (value.charAt(1) == 'x' || value.charAt(1) == 'X')) {
// allow hex values starting with 0x or 0X
radix = 16;
value = value.substring(2);
}
try {
intValue = Integer.parseInt(value, radix);
} catch (NumberFormatException e) {
Log.e(TAG, "invalid number for field " + name, e);
continue;
}
if ("vendor-id".equals(name)) {
vendorId = intValue;
} else if ("product-id".equals(name)) {
productId = intValue;
} else if ("class".equals(name)) {
deviceClass = intValue;
} else if ("subclass".equals(name)) {
deviceSubclass = intValue;
} else if ("protocol".equals(name)) {
deviceProtocol = intValue;
}
}
}
return new DeviceFilter(vendorId, productId,
deviceClass, deviceSubclass, deviceProtocol,
manufacturerName, productName, serialNumber, aoapManufacturer,
aoapModel, aoapDescription, aoapVersion, aoapUri, aoapSerial,
aoapService);
}
private boolean matches(int clasz, int subclass, int protocol) {
return ((mClass == -1 || clasz == mClass)
&& (mSubclass == -1 || subclass == mSubclass)
&& (mProtocol == -1 || protocol == mProtocol));
}
public boolean matches(UsbDevice device) {
if (mVendorId != -1 && device.getVendorId() != mVendorId) {
return false;
}
if (mProductId != -1 && device.getProductId() != mProductId) {
return false;
}
if (mManufacturerName != null && device.getManufacturerName() == null) {
return false;
}
if (mProductName != null && device.getProductName() == null) {
return false;
}
if (mSerialNumber != null && device.getSerialNumber() == null) {
return false;
}
if (mManufacturerName != null && device.getManufacturerName() != null
&& !mManufacturerName.equals(device.getManufacturerName())) {
return false;
}
if (mProductName != null && device.getProductName() != null
&& !mProductName.equals(device.getProductName())) {
return false;
}
if (mSerialNumber != null && device.getSerialNumber() != null
&& !mSerialNumber.equals(device.getSerialNumber())) {
return false;
}
// check device class/subclass/protocol
if (matches(device.getDeviceClass(), device.getDeviceSubclass(),
device.getDeviceProtocol())) {
return true;
}
// if device doesn't match, check the interfaces
int count = device.getInterfaceCount();
for (int i = 0; i < count; i++) {
UsbInterface intf = device.getInterface(i);
if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(),
intf.getInterfaceProtocol())) {
return true;
}
}
return false;
}
@Override
public boolean equals(Object obj) {
// can't compare if we have wildcard strings
if (mVendorId == -1 || mProductId == -1
|| mClass == -1 || mSubclass == -1 || mProtocol == -1) {
return false;
}
if (obj instanceof DeviceFilter) {
DeviceFilter filter = (DeviceFilter) obj;
if (filter.mVendorId != mVendorId
|| filter.mProductId != mProductId
|| filter.mClass != mClass
|| filter.mSubclass != mSubclass
|| filter.mProtocol != mProtocol) {
return false;
}
if ((filter.mManufacturerName != null && mManufacturerName == null)
|| (filter.mManufacturerName == null && mManufacturerName != null)
|| (filter.mProductName != null && mProductName == null)
|| (filter.mProductName == null && mProductName != null)
|| (filter.mSerialNumber != null && mSerialNumber == null)
|| (filter.mSerialNumber == null && mSerialNumber != null)) {
return false;
}
if ((filter.mManufacturerName != null && mManufacturerName != null
&& !mManufacturerName.equals(filter.mManufacturerName))
|| (filter.mProductName != null && mProductName != null
&& !mProductName.equals(filter.mProductName))
|| (filter.mSerialNumber != null && mSerialNumber != null
&& !mSerialNumber.equals(filter.mSerialNumber))) {
return false;
}
return true;
}
if (obj instanceof UsbDevice) {
UsbDevice device = (UsbDevice) obj;
if (device.getVendorId() != mVendorId
|| device.getProductId() != mProductId
|| device.getDeviceClass() != mClass
|| device.getDeviceSubclass() != mSubclass
|| device.getDeviceProtocol() != mProtocol) {
return false;
}
if ((mManufacturerName != null && device.getManufacturerName() == null)
|| (mManufacturerName == null && device.getManufacturerName() != null)
|| (mProductName != null && device.getProductName() == null)
|| (mProductName == null && device.getProductName() != null)
|| (mSerialNumber != null && device.getSerialNumber() == null)
|| (mSerialNumber == null && device.getSerialNumber() != null)) {
return false;
}
if ((device.getManufacturerName() != null
&& !mManufacturerName.equals(device.getManufacturerName()))
|| (device.getProductName() != null
&& !mProductName.equals(device.getProductName()))
|| (device.getSerialNumber() != null
&& !mSerialNumber.equals(device.getSerialNumber()))) {
return false;
}
return true;
}
return false;
}
@Override
public int hashCode() {
return (((mVendorId << 16) | mProductId)
^ ((mClass << 16) | (mSubclass << 8) | mProtocol));
}
@Override
public String toString() {
return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId=" + mProductId
+ ",mClass=" + mClass + ",mSubclass=" + mSubclass
+ ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName
+ ",mProductName=" + mProductName + ",mSerialNumber=" + mSerialNumber + "]";
}
}
public UsbDeviceHandlerResolver(UsbManager manager, Context context,
UsbDeviceHandlerResolverCallback deviceListener) {
mUsbManager = manager;
mContext = context;
mDeviceCallback = deviceListener;
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new UsbDeviceResolverHandler(mHandlerThread.getLooper());
mPackageManager = context.getPackageManager();
}
/**
* Releases current object.
*/
public void release() {
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
}
}
/**
* Resolves handlers for USB device.
*/
public void resolve(UsbDevice device) {
mHandler.requestResolveHandlers(device);
}
/**
* Dispatches device to component.
*/
public boolean dispatch(UsbDevice device, ComponentName component, boolean inAoap) {
if (LOCAL_LOGD) {
Log.d(TAG, "dispatch: " + device + " component: " + component + " inAoap: " + inAoap);
}
ActivityInfo activityInfo;
try {
activityInfo = mPackageManager.getActivityInfo(component, PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
Log.e(TAG, "Activity not found: " + component);
return false;
}
Intent intent = createDeviceAttachedIntent(device);
if (inAoap) {
if (AoapInterface.isDeviceInAoapMode(device)) {
mDeviceCallback.onDeviceDispatched();
} else {
DeviceFilter filter =
packageMatches(activityInfo, intent.getAction(), device, true);
if (filter != null) {
requestAoapSwitch(device, filter);
return true;
}
}
}
intent.setComponent(component);
mUsbManager.grantPermission(device, activityInfo.applicationInfo.uid);
mContext.startActivity(intent);
mHandler.requestCompleteDeviceDispatch();
return true;
}
private static Intent createDeviceAttachedIntent(UsbDevice device) {
Intent intent = new Intent(UsbManager.ACTION_USB_DEVICE_ATTACHED);
intent.putExtra(UsbManager.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
private void doHandleResolveHandlers(UsbDevice device) {
if (LOCAL_LOGD) {
Log.d(TAG, "doHandleResolveHandlers: " + device);
}
Intent intent = createDeviceAttachedIntent(device);
List<Pair<ResolveInfo, DeviceFilter>> matches = getDeviceMatches(device, intent, false);
if (LOCAL_LOGD) {
Log.d(TAG, "matches size: " + matches.size());
}
List<UsbDeviceSettings> settings = new ArrayList<>(matches.size());
for (Pair<ResolveInfo, DeviceFilter> info : matches) {
UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(device);
setting.setHandler(
new ComponentName(
info.first.activityInfo.packageName, info.first.activityInfo.name));
settings.add(setting);
}
DeviceContext deviceContext =
new DeviceContext(device, UsbDeviceSettings.constructSettings(device), settings);
if (AoapInterface.isSupported(deviceContext.connection)) {
deviceContext.mActiveDeviceOptions.addAll(getDeviceMatches(device, intent, true));
queryNextAoapHandler(deviceContext);
} else {
deviceProbingComplete(deviceContext);
}
}
private void queryNextAoapHandler(DeviceContext context) {
Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
if (option == null) {
Log.w(TAG, "No more options left.");
deviceProbingComplete(context);
return;
}
Intent serviceIntent = new Intent();
serviceIntent.setComponent(ComponentName.unflattenFromString(option.second.mAoapService));
boolean bound = mContext.bindService(serviceIntent, context.mServiceConnection,
Context.BIND_AUTO_CREATE);
if (bound) {
mHandler.requestServiceConnectionTimeout();
} else {
if (LOCAL_LOGD) {
Log.d(TAG, "Failed to bind to the service");
}
context.mActiveDeviceOptions.poll();
queryNextAoapHandler(context);
}
}
private void requestAoapSwitch(UsbDevice device, DeviceFilter filter) {
UsbDeviceConnection connection = UsbUtil.openConnection(mUsbManager, device);
try {
UsbUtil.sendAoapAccessoryStart(
connection,
filter.mAoapManufacturer,
filter.mAoapModel,
filter.mAoapDescription,
filter.mAoapVersion,
filter.mAoapUri,
filter.mAoapSerial);
} catch (IOException e) {
Log.w(TAG, "Failed to switch device into AOAP mode", e);
}
connection.close();
}
private void deviceProbingComplete(DeviceContext context) {
if (LOCAL_LOGD) {
Log.d(TAG, "deviceProbingComplete");
}
mDeviceCallback.onHandlersResolveCompleted(context.usbDevice, context.activeDeviceSettings);
}
private void doHandleServiceConnectionStateChanged(DeviceContext context) {
if (LOCAL_LOGD) {
Log.d(TAG, "doHandleServiceConnectionStateChanged: "
+ context.mUsbAoapSupportCheckService);
}
if (context.mUsbAoapSupportCheckService != null) {
boolean deviceSupported = false;
try {
deviceSupported =
context.mUsbAoapSupportCheckService.isDeviceSupported(context.usbDevice);
} catch (RemoteException e) {
Log.e(TAG, "Call to remote service failed", e);
}
if (deviceSupported) {
Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(context.settings);
setting.setHandler(
new ComponentName(
option.first.activityInfo.packageName, option.first.activityInfo.name));
setting.setAoap(true);
context.activeDeviceSettings.add(setting);
}
mContext.unbindService(context.mServiceConnection);
}
context.mActiveDeviceOptions.poll();
queryNextAoapHandler(context);
}
private List<Pair<ResolveInfo, DeviceFilter>> getDeviceMatches(
UsbDevice device, Intent intent, boolean forAoap) {
List<Pair<ResolveInfo, DeviceFilter>> matches = new ArrayList<>();
List<ResolveInfo> resolveInfos =
mPackageManager.queryIntentActivities(intent, PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
DeviceFilter filter = packageMatches(resolveInfo.activityInfo,
intent.getAction(), device, forAoap);
if (filter != null) {
matches.add(Pair.create(resolveInfo, filter));
}
}
return matches;
}
private DeviceFilter packageMatches(ActivityInfo ai, String metaDataName, UsbDevice device,
boolean forAoap) {
if (LOCAL_LOGD) {
Log.d(TAG, "packageMatches ai: " + ai + "metaDataName: " + metaDataName + " forAoap: "
+ forAoap);
}
String filterTagName = forAoap ? "usb-aoap-accessory" : "usb-device";
XmlResourceParser parser = null;
try {
parser = ai.loadXmlMetaData(mPackageManager, metaDataName);
if (parser == null) {
Log.w(TAG, "no meta-data for " + ai);
return null;
}
XmlUtils.nextElement(parser);
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
if (device != null && filterTagName.equals(tagName)) {
DeviceFilter filter = DeviceFilter.read(parser, forAoap);
if (forAoap || filter.matches(device)) {
return filter;
}
}
XmlUtils.nextElement(parser);
}
} catch (Exception e) {
Log.w(TAG, "Unable to load component info " + ai.toString(), e);
} finally {
if (parser != null) parser.close();
}
return null;
}
private class UsbDeviceResolverHandler extends Handler {
private static final int MSG_RESOLVE_HANDLERS = 0;
private static final int MSG_SERVICE_CONNECTION_STATE_CHANGE = 1;
private static final int MSG_SERVICE_CONNECTION_TIMEOUT = 2;
private static final int MSG_COMPLETE_DISPATCH = 3;
private static final long CONNECT_TIMEOUT_MS = 5000;
private UsbDeviceResolverHandler(Looper looper) {
super(looper);
}
public void requestResolveHandlers(UsbDevice device) {
Message msg = obtainMessage(MSG_RESOLVE_HANDLERS, device);
sendMessage(msg);
}
public void requestOnServiceConnectionStateChanged(DeviceContext deviceContext) {
sendMessage(obtainMessage(MSG_SERVICE_CONNECTION_STATE_CHANGE, deviceContext));
}
public void requestServiceConnectionTimeout() {
sendEmptyMessageDelayed(MSG_SERVICE_CONNECTION_TIMEOUT, CONNECT_TIMEOUT_MS);
}
public void requestCompleteDeviceDispatch() {
sendEmptyMessage(MSG_COMPLETE_DISPATCH);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RESOLVE_HANDLERS:
doHandleResolveHandlers((UsbDevice) msg.obj);
break;
case MSG_SERVICE_CONNECTION_STATE_CHANGE:
removeMessages(MSG_SERVICE_CONNECTION_TIMEOUT);
doHandleServiceConnectionStateChanged((DeviceContext) msg.obj);
break;
case MSG_SERVICE_CONNECTION_TIMEOUT:
Log.i(TAG, "Service connection timeout");
doHandleServiceConnectionStateChanged(null);
break;
case MSG_COMPLETE_DISPATCH:
mDeviceCallback.onDeviceDispatched();
break;
default:
Log.w(TAG, "Unsupported message: " + msg);
}
}
}
}