/* * 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 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 activeDeviceSettings; public final Queue> 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 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> matches = getDeviceMatches(device, intent, false); if (LOCAL_LOGD) { Log.d(TAG, "matches size: " + matches.size()); } List settings = new ArrayList<>(matches.size()); for (Pair 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 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 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> getDeviceMatches( UsbDevice device, Intent intent, boolean forAoap) { List> matches = new ArrayList<>(); List 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); } } } }