blob: 43d5bf323abbd0e354a20adc231e3bfa82ed2501 [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.server.usb.descriptors;
import android.hardware.usb.UsbDevice;
import android.util.Log;
import java.util.ArrayList;
/**
* @hide
* Class for parsing a binary stream of USB Descriptors.
*/
public final class UsbDescriptorParser {
private static final String TAG = "UsbDescriptorParser";
public static final boolean DEBUG = false;
private final String mDeviceAddr;
// Descriptor Objects
private static final int DESCRIPTORS_ALLOC_SIZE = 128;
private final ArrayList<UsbDescriptor> mDescriptors;
private UsbDeviceDescriptor mDeviceDescriptor;
private UsbConfigDescriptor mCurConfigDescriptor;
private UsbInterfaceDescriptor mCurInterfaceDescriptor;
// The AudioClass spec implemented by the AudioClass Interfaces
// This may well be different than the overall USB Spec.
// Obtained from the first AudioClass Header descriptor.
private int mACInterfacesSpec = UsbDeviceDescriptor.USBSPEC_1_0;
// The VideoClass spec implemented by the VideoClass Interfaces
// This may well be different than the overall USB Spec.
// Obtained from the first VidieoClass Header descriptor.
private int mVCInterfacesSpec = UsbDeviceDescriptor.USBSPEC_1_0;
/**
* Connect this parser to an existing set of already parsed descriptors.
* This is useful for reporting.
*/
public UsbDescriptorParser(String deviceAddr, ArrayList<UsbDescriptor> descriptors) {
mDeviceAddr = deviceAddr;
mDescriptors = descriptors;
//TODO some error checking here....
mDeviceDescriptor = (UsbDeviceDescriptor) descriptors.get(0);
}
/**
* Connect this parser to an byte array containing unparsed (raw) device descriptors
* to be parsed (and parse them). Useful for parsing a stored descriptor buffer.
*/
public UsbDescriptorParser(String deviceAddr, byte[] rawDescriptors) {
mDeviceAddr = deviceAddr;
mDescriptors = new ArrayList<UsbDescriptor>(DESCRIPTORS_ALLOC_SIZE);
parseDescriptors(rawDescriptors);
}
public String getDeviceAddr() {
return mDeviceAddr;
}
/**
* @return the USB Spec value associated with the Device descriptor for the
* descriptors stream being parsed.
*
* @throws IllegalArgumentException
*/
public int getUsbSpec() {
if (mDeviceDescriptor != null) {
return mDeviceDescriptor.getSpec();
} else {
throw new IllegalArgumentException();
}
}
public void setACInterfaceSpec(int spec) {
mACInterfacesSpec = spec;
}
public int getACInterfaceSpec() {
return mACInterfacesSpec;
}
public void setVCInterfaceSpec(int spec) {
mVCInterfacesSpec = spec;
}
public int getVCInterfaceSpec() {
return mVCInterfacesSpec;
}
private class UsbDescriptorsStreamFormatException extends Exception {
String mMessage;
UsbDescriptorsStreamFormatException(String message) {
mMessage = message;
}
public String toString() {
return "Descriptor Stream Format Exception: " + mMessage;
}
}
/**
* The probability (as returned by getHeadsetProbability() at which we conclude
* the peripheral is a headset.
*/
private static final float IN_HEADSET_TRIGGER = 0.75f;
private static final float OUT_HEADSET_TRIGGER = 0.75f;
private UsbDescriptor allocDescriptor(ByteStream stream)
throws UsbDescriptorsStreamFormatException {
stream.resetReadCount();
int length = stream.getUnsignedByte();
byte type = stream.getByte();
UsbDescriptor.logDescriptorName(type, length);
UsbDescriptor descriptor = null;
switch (type) {
/*
* Standard
*/
case UsbDescriptor.DESCRIPTORTYPE_DEVICE:
descriptor = mDeviceDescriptor = new UsbDeviceDescriptor(length, type);
break;
case UsbDescriptor.DESCRIPTORTYPE_CONFIG:
descriptor = mCurConfigDescriptor = new UsbConfigDescriptor(length, type);
if (mDeviceDescriptor != null) {
mDeviceDescriptor.addConfigDescriptor(mCurConfigDescriptor);
} else {
Log.e(TAG, "Config Descriptor found with no associated Device Descriptor!");
throw new UsbDescriptorsStreamFormatException(
"Config Descriptor found with no associated Device Descriptor!");
}
break;
case UsbDescriptor.DESCRIPTORTYPE_INTERFACE:
descriptor = mCurInterfaceDescriptor = new UsbInterfaceDescriptor(length, type);
if (mCurConfigDescriptor != null) {
mCurConfigDescriptor.addInterfaceDescriptor(mCurInterfaceDescriptor);
} else {
Log.e(TAG, "Interface Descriptor found with no associated Config Descriptor!");
throw new UsbDescriptorsStreamFormatException(
"Interface Descriptor found with no associated Config Descriptor!");
}
break;
case UsbDescriptor.DESCRIPTORTYPE_ENDPOINT:
descriptor = new UsbEndpointDescriptor(length, type);
if (mCurInterfaceDescriptor != null) {
mCurInterfaceDescriptor.addEndpointDescriptor(
(UsbEndpointDescriptor) descriptor);
} else {
Log.e(TAG,
"Endpoint Descriptor found with no associated Interface Descriptor!");
throw new UsbDescriptorsStreamFormatException(
"Endpoint Descriptor found with no associated Interface Descriptor!");
}
break;
/*
* HID
*/
case UsbDescriptor.DESCRIPTORTYPE_HID:
descriptor = new UsbHIDDescriptor(length, type);
break;
/*
* Other
*/
case UsbDescriptor.DESCRIPTORTYPE_INTERFACEASSOC:
descriptor = new UsbInterfaceAssoc(length, type);
break;
/*
* Various Class Specific
*/
case UsbDescriptor.DESCRIPTORTYPE_CLASSSPECIFIC_INTERFACE:
if (mCurInterfaceDescriptor != null) {
switch (mCurInterfaceDescriptor.getUsbClass()) {
case UsbDescriptor.CLASSID_AUDIO:
descriptor = UsbACInterface.allocDescriptor(this, stream, length, type);
break;
case UsbDescriptor.CLASSID_VIDEO:
if (DEBUG) {
Log.d(TAG, " UsbDescriptor.CLASSID_VIDEO");
}
descriptor = UsbVCInterface.allocDescriptor(this, stream, length, type);
break;
case UsbDescriptor.CLASSID_AUDIOVIDEO:
if (DEBUG) {
Log.d(TAG, " UsbDescriptor.CLASSID_AUDIOVIDEO");
}
break;
default:
Log.w(TAG, " Unparsed Class-specific");
break;
}
}
break;
case UsbDescriptor.DESCRIPTORTYPE_CLASSSPECIFIC_ENDPOINT:
if (mCurInterfaceDescriptor != null) {
int subClass = mCurInterfaceDescriptor.getUsbClass();
switch (subClass) {
case UsbDescriptor.CLASSID_AUDIO:
descriptor = UsbACEndpoint.allocDescriptor(this, length, type);
break;
case UsbDescriptor.CLASSID_VIDEO: {
Byte subtype = stream.getByte();
if (DEBUG) {
Log.d(TAG, "UsbDescriptor.CLASSID_VIDEO type:0x"
+ Integer.toHexString(type));
}
descriptor = UsbVCEndpoint.allocDescriptor(this, length, type, subtype);
}
break;
case UsbDescriptor.CLASSID_AUDIOVIDEO:
if (DEBUG) {
Log.d(TAG, "UsbDescriptor.CLASSID_AUDIOVIDEO type:0x"
+ Integer.toHexString(type));
}
break;
default:
Log.w(TAG, " Unparsed Class-specific Endpoint:0x"
+ Integer.toHexString(subClass));
break;
}
}
break;
default:
break;
}
if (descriptor == null) {
// Unknown Descriptor
descriptor = new UsbUnknown(length, type);
}
return descriptor;
}
public UsbDeviceDescriptor getDeviceDescriptor() {
return mDeviceDescriptor;
}
public UsbInterfaceDescriptor getCurInterface() {
return mCurInterfaceDescriptor;
}
/**
* @hide
*/
public void parseDescriptors(byte[] descriptors) {
ByteStream stream = new ByteStream(descriptors);
while (stream.available() > 0) {
UsbDescriptor descriptor = null;
try {
descriptor = allocDescriptor(stream);
} catch (Exception ex) {
Log.e(TAG, "Exception allocating USB descriptor.", ex);
}
if (descriptor != null) {
// Parse
try {
descriptor.parseRawDescriptors(stream);
// Clean up
descriptor.postParse(stream);
} catch (Exception ex) {
// Clean up, compute error status
descriptor.postParse(stream);
// Report
Log.w(TAG, "Exception parsing USB descriptors. type:0x" + descriptor.getType()
+ " status:" + descriptor.getStatus());
if (DEBUG) {
// Show full stack trace if debugging
Log.e(TAG, "Exception parsing USB descriptors.", ex);
}
StackTraceElement[] stackElems = ex.getStackTrace();
if (stackElems.length > 0) {
Log.i(TAG, " class:" + stackElems[0].getClassName()
+ " @ " + stackElems[0].getLineNumber());
}
if (stackElems.length > 1) {
Log.i(TAG, " class:" + stackElems[1].getClassName()
+ " @ " + stackElems[1].getLineNumber());
}
// Finish up
descriptor.setStatus(UsbDescriptor.STATUS_PARSE_EXCEPTION);
} finally {
mDescriptors.add(descriptor);
}
}
}
if (DEBUG) {
Log.d(TAG, "parseDescriptors() - end " + mDescriptors.size() + " descriptors.");
}
}
public byte[] getRawDescriptors() {
return getRawDescriptors_native(mDeviceAddr);
}
private native byte[] getRawDescriptors_native(String deviceAddr);
/**
* @hide
*/
public String getDescriptorString(int stringId) {
return getDescriptorString_native(mDeviceAddr, stringId);
}
private native String getDescriptorString_native(String deviceAddr, int stringId);
public int getParsingSpec() {
return mDeviceDescriptor != null ? mDeviceDescriptor.getSpec() : 0;
}
public ArrayList<UsbDescriptor> getDescriptors() {
return mDescriptors;
}
/**
* @hide
*/
public UsbDevice.Builder toAndroidUsbDeviceBuilder() {
if (mDeviceDescriptor == null) {
Log.e(TAG, "toAndroidUsbDevice() ERROR - No Device Descriptor");
return null;
}
UsbDevice.Builder builder = mDeviceDescriptor.toAndroid(this);
if (builder == null) {
Log.e(TAG, "toAndroidUsbDevice() ERROR Creating Device");
}
return builder;
}
/**
* @hide
*/
public ArrayList<UsbDescriptor> getDescriptors(byte type) {
ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
for (UsbDescriptor descriptor : mDescriptors) {
if (descriptor.getType() == type) {
list.add(descriptor);
}
}
return list;
}
/**
* @hide
*/
public ArrayList<UsbDescriptor> getInterfaceDescriptorsForClass(int usbClass) {
ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
for (UsbDescriptor descriptor : mDescriptors) {
// ensure that this isn't an unrecognized DESCRIPTORTYPE_INTERFACE
if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_INTERFACE) {
if (descriptor instanceof UsbInterfaceDescriptor) {
UsbInterfaceDescriptor intrDesc = (UsbInterfaceDescriptor) descriptor;
if (intrDesc.getUsbClass() == usbClass) {
list.add(descriptor);
}
} else {
Log.w(TAG, "Unrecognized Interface l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
}
return list;
}
/**
* @hide
*/
public ArrayList<UsbDescriptor> getACInterfaceDescriptors(byte subtype, int subclass) {
ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
for (UsbDescriptor descriptor : mDescriptors) {
if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_CLASSSPECIFIC_INTERFACE) {
// ensure that this isn't an unrecognized DESCRIPTORTYPE_CLASSSPECIFIC_INTERFACE
if (descriptor instanceof UsbACInterface) {
UsbACInterface acDescriptor = (UsbACInterface) descriptor;
if (acDescriptor.getSubtype() == subtype
&& acDescriptor.getSubclass() == subclass) {
list.add(descriptor);
}
} else {
Log.w(TAG, "Unrecognized Audio Interface len: " + descriptor.getLength()
+ " type:0x" + Integer.toHexString(descriptor.getType()));
}
}
}
return list;
}
/*
* Attribute predicates
*/
/**
* @hide
*/
public boolean hasInput() {
if (DEBUG) {
Log.d(TAG, "---- hasInput()");
}
ArrayList<UsbDescriptor> acDescriptors =
getACInterfaceDescriptors(UsbACInterface.ACI_INPUT_TERMINAL,
UsbACInterface.AUDIO_AUDIOCONTROL);
boolean hasInput = false;
for (UsbDescriptor descriptor : acDescriptors) {
if (descriptor instanceof UsbACTerminal) {
UsbACTerminal inDescr = (UsbACTerminal) descriptor;
// Check for input and bi-directional terminal types
int type = inDescr.getTerminalType();
if (DEBUG) {
Log.d(TAG, " type:0x" + Integer.toHexString(type));
}
int terminalCategory = type & ~0xFF;
if (terminalCategory != UsbTerminalTypes.TERMINAL_USB_UNDEFINED
&& terminalCategory != UsbTerminalTypes.TERMINAL_OUT_UNDEFINED) {
// If not explicitly a USB connection or output, it could be an input.
hasInput = true;
break;
}
} else {
Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
if (DEBUG) {
Log.d(TAG, "hasInput() = " + hasInput);
}
return hasInput;
}
/**
* @hide
*/
public boolean hasOutput() {
if (DEBUG) {
Log.d(TAG, "---- hasOutput()");
}
ArrayList<UsbDescriptor> acDescriptors =
getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
UsbACInterface.AUDIO_AUDIOCONTROL);
boolean hasOutput = false;
for (UsbDescriptor descriptor : acDescriptors) {
if (descriptor instanceof UsbACTerminal) {
UsbACTerminal outDescr = (UsbACTerminal) descriptor;
// Check for output and bi-directional terminal types
int type = outDescr.getTerminalType();
if (DEBUG) {
Log.d(TAG, " type:0x" + Integer.toHexString(type));
}
int terminalCategory = type & ~0xFF;
if (terminalCategory != UsbTerminalTypes.TERMINAL_USB_UNDEFINED
&& terminalCategory != UsbTerminalTypes.TERMINAL_IN_UNDEFINED) {
// If not explicitly a USB connection or input, it could be an output.
hasOutput = true;
break;
}
} else {
Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
if (DEBUG) {
Log.d(TAG, "hasOutput() = " + hasOutput);
}
return hasOutput;
}
/**
* @hide
*/
public boolean hasMic() {
boolean hasMic = false;
ArrayList<UsbDescriptor> acDescriptors =
getACInterfaceDescriptors(UsbACInterface.ACI_INPUT_TERMINAL,
UsbACInterface.AUDIO_AUDIOCONTROL);
for (UsbDescriptor descriptor : acDescriptors) {
if (descriptor instanceof UsbACTerminal) {
UsbACTerminal inDescr = (UsbACTerminal) descriptor;
if (inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_IN_MIC
|| inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET
|| inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_UNDEFINED
|| inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_EXTERN_LINE) {
hasMic = true;
break;
}
} else {
Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
return hasMic;
}
/**
* @hide
*/
public boolean hasSpeaker() {
boolean hasSpeaker = false;
ArrayList<UsbDescriptor> acDescriptors =
getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
UsbACInterface.AUDIO_AUDIOCONTROL);
for (UsbDescriptor descriptor : acDescriptors) {
if (descriptor instanceof UsbACTerminal) {
UsbACTerminal outDescr = (UsbACTerminal) descriptor;
if (outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_SPEAKER
|| outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_HEADPHONES
|| outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET) {
hasSpeaker = true;
break;
}
} else {
Log.w(TAG, "Undefined Audio Output terminal l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
return hasSpeaker;
}
/**
*@ hide
*/
public boolean hasAudioInterface() {
ArrayList<UsbDescriptor> descriptors =
getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO);
return !descriptors.isEmpty();
}
/**
* @hide
*/
public boolean hasAudioTerminal(int subType) {
for (UsbDescriptor descriptor : mDescriptors) {
if (descriptor instanceof UsbACInterface) {
if (((UsbACInterface) descriptor).getSubclass()
== UsbDescriptor.AUDIO_AUDIOCONTROL
&& ((UsbACInterface) descriptor).getSubtype()
== subType) {
return true;
}
}
}
return false;
}
/**
* @hide
*/
public boolean hasAudioPlayback() {
return hasAudioTerminal(UsbACInterface.ACI_OUTPUT_TERMINAL);
}
/**
* @hide
*/
public boolean hasAudioCapture() {
return hasAudioTerminal(UsbACInterface.ACI_INPUT_TERMINAL);
}
/**
* @hide
*/
public boolean hasVideoCapture() {
for (UsbDescriptor descriptor : mDescriptors) {
if (descriptor instanceof UsbVCInputTerminal) {
return true;
}
}
return false;
}
/**
* @hide
*/
public boolean hasVideoPlayback() {
for (UsbDescriptor descriptor : mDescriptors) {
if (descriptor instanceof UsbVCOutputTerminal) {
return true;
}
}
return false;
}
/**
* @hide
*/
public boolean hasHIDInterface() {
ArrayList<UsbDescriptor> descriptors =
getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_HID);
return !descriptors.isEmpty();
}
/**
* @hide
*/
public boolean hasStorageInterface() {
ArrayList<UsbDescriptor> descriptors =
getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_STORAGE);
return !descriptors.isEmpty();
}
/**
* @hide
*/
public boolean hasMIDIInterface() {
ArrayList<UsbDescriptor> descriptors =
getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO);
for (UsbDescriptor descriptor : descriptors) {
// enusure that this isn't an unrecognized interface descriptor
if (descriptor instanceof UsbInterfaceDescriptor) {
UsbInterfaceDescriptor interfaceDescr = (UsbInterfaceDescriptor) descriptor;
if (interfaceDescr.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) {
return true;
}
} else {
Log.w(TAG, "Undefined Audio Class Interface l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
return false;
}
/**
* @hide
*/
public float getInputHeadsetProbability() {
if (hasMIDIInterface()) {
return 0.0f;
}
float probability = 0.0f;
// Look for a microphone
boolean hasMic = hasMic();
// Look for a "speaker"
boolean hasSpeaker = hasSpeaker();
if (hasMic && hasSpeaker) {
probability += 0.75f;
}
if (hasMic && hasHIDInterface()) {
probability += 0.25f;
}
return probability;
}
/**
* getInputHeadsetProbability() reports a probability of a USB Input peripheral being a
* headset. The probability range is between 0.0f (definitely NOT a headset) and
* 1.0f (definitely IS a headset). A probability of 0.75f seems sufficient
* to count on the peripheral being a headset.
*/
public boolean isInputHeadset() {
return getInputHeadsetProbability() >= IN_HEADSET_TRIGGER;
}
/**
* @hide
*/
public float getOutputHeadsetProbability() {
if (hasMIDIInterface()) {
return 0.0f;
}
float probability = 0.0f;
ArrayList<UsbDescriptor> acDescriptors;
// Look for a "speaker"
boolean hasSpeaker = false;
acDescriptors =
getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
UsbACInterface.AUDIO_AUDIOCONTROL);
for (UsbDescriptor descriptor : acDescriptors) {
if (descriptor instanceof UsbACTerminal) {
UsbACTerminal outDescr = (UsbACTerminal) descriptor;
if (outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_SPEAKER
|| outDescr.getTerminalType()
== UsbTerminalTypes.TERMINAL_OUT_HEADPHONES
|| outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET) {
hasSpeaker = true;
break;
}
} else {
Log.w(TAG, "Undefined Audio Output terminal l: " + descriptor.getLength()
+ " t:0x" + Integer.toHexString(descriptor.getType()));
}
}
if (hasSpeaker) {
probability += 0.75f;
}
if (hasSpeaker && hasHIDInterface()) {
probability += 0.25f;
}
return probability;
}
/**
* getOutputHeadsetProbability() reports a probability of a USB Output peripheral being a
* headset. The probability range is between 0.0f (definitely NOT a headset) and
* 1.0f (definitely IS a headset). A probability of 0.75f seems sufficient
* to count on the peripheral being a headset.
*/
public boolean isOutputHeadset() {
return getOutputHeadsetProbability() >= OUT_HEADSET_TRIGGER;
}
}