These extras are also accessable via the UsbDevice and UsbAccessory classes, which are also included as extras. Since you can't filter Intents based on extras there is no point in duplicating this information as separate extras. Change-Id: I4c8d1e70d66023a1800b3f8f06118898da6b37af Signed-off-by: Mike Lockwood <lockwood@android.com>
523 lines
22 KiB
Java
523 lines
22 KiB
Java
/*
|
|
* Copyright (C) 2010 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;
|
|
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.hardware.IUsbManager;
|
|
import android.hardware.UsbAccessory;
|
|
import android.hardware.UsbConstants;
|
|
import android.hardware.UsbDevice;
|
|
import android.hardware.UsbEndpoint;
|
|
import android.hardware.UsbInterface;
|
|
import android.hardware.UsbManager;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.Parcelable;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.UEventObserver;
|
|
import android.provider.Settings;
|
|
import android.util.Log;
|
|
import android.util.Slog;
|
|
|
|
import java.io.File;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileReader;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
|
|
/**
|
|
* UsbService monitors for changes to USB state.
|
|
* This includes code for both USB host support (where the android device is the host)
|
|
* as well as USB device support (android device is connected to a USB host).
|
|
* Accessory mode is a special case of USB device mode, where the android device is
|
|
* connected to a USB host that supports the android accessory protocol.
|
|
*/
|
|
class UsbService extends IUsbManager.Stub {
|
|
private static final String TAG = UsbService.class.getSimpleName();
|
|
private static final boolean LOG = false;
|
|
|
|
private static final String USB_CONNECTED_MATCH =
|
|
"DEVPATH=/devices/virtual/switch/usb_connected";
|
|
private static final String USB_CONFIGURATION_MATCH =
|
|
"DEVPATH=/devices/virtual/switch/usb_configuration";
|
|
private static final String USB_FUNCTIONS_MATCH =
|
|
"DEVPATH=/devices/virtual/usb_composite/";
|
|
private static final String USB_CONNECTED_PATH =
|
|
"/sys/class/switch/usb_connected/state";
|
|
private static final String USB_CONFIGURATION_PATH =
|
|
"/sys/class/switch/usb_configuration/state";
|
|
private static final String USB_COMPOSITE_CLASS_PATH =
|
|
"/sys/class/usb_composite";
|
|
|
|
private static final int MSG_UPDATE_STATE = 0;
|
|
private static final int MSG_FUNCTION_ENABLED = 1;
|
|
private static final int MSG_FUNCTION_DISABLED = 2;
|
|
|
|
// Delay for debouncing USB disconnects.
|
|
// We often get rapid connect/disconnect events when enabling USB functions,
|
|
// which need debouncing.
|
|
private static final int UPDATE_DELAY = 1000;
|
|
|
|
// current connected and configuration state
|
|
private int mConnected;
|
|
private int mConfiguration;
|
|
|
|
// last broadcasted connected and configuration state
|
|
private int mLastConnected = -1;
|
|
private int mLastConfiguration = -1;
|
|
|
|
// lists of enabled and disabled USB functions (for USB device mode)
|
|
private final ArrayList<String> mEnabledFunctions = new ArrayList<String>();
|
|
private final ArrayList<String> mDisabledFunctions = new ArrayList<String>();
|
|
|
|
// contains all connected USB devices (for USB host mode)
|
|
private final HashMap<String,UsbDevice> mDevices = new HashMap<String,UsbDevice>();
|
|
|
|
// USB busses to exclude from USB host support
|
|
private final String[] mHostBlacklist;
|
|
|
|
private boolean mSystemReady;
|
|
|
|
private UsbAccessory mCurrentAccessory;
|
|
// functions to restore after exiting accessory mode
|
|
private final ArrayList<String> mAccessoryRestoreFunctions = new ArrayList<String>();
|
|
|
|
private final Context mContext;
|
|
private final Object mLock = new Object();
|
|
|
|
/*
|
|
* Handles USB function enable/disable events (device mode)
|
|
*/
|
|
private final void functionEnabledLocked(String function, boolean enabled) {
|
|
boolean enteringAccessoryMode =
|
|
(enabled && UsbManager.USB_FUNCTION_ACCESSORY.equals(function));
|
|
|
|
if (enteringAccessoryMode) {
|
|
// keep a list of functions to reenable after exiting accessory mode
|
|
mAccessoryRestoreFunctions.clear();
|
|
int count = mEnabledFunctions.size();
|
|
for (int i = 0; i < count; i++) {
|
|
String f = mEnabledFunctions.get(i);
|
|
// RNDIS should not be restored and adb is handled automatically
|
|
if (!UsbManager.USB_FUNCTION_RNDIS.equals(f) &&
|
|
!UsbManager.USB_FUNCTION_ADB.equals(f) &&
|
|
!UsbManager.USB_FUNCTION_ACCESSORY.equals(f)) {
|
|
mAccessoryRestoreFunctions.add(f);
|
|
}
|
|
}
|
|
}
|
|
if (enabled) {
|
|
if (!mEnabledFunctions.contains(function)) {
|
|
mEnabledFunctions.add(function);
|
|
}
|
|
mDisabledFunctions.remove(function);
|
|
} else {
|
|
if (!mDisabledFunctions.contains(function)) {
|
|
mDisabledFunctions.add(function);
|
|
}
|
|
mEnabledFunctions.remove(function);
|
|
}
|
|
|
|
if (enteringAccessoryMode) {
|
|
String[] strings = nativeGetAccessoryStrings();
|
|
if (strings != null) {
|
|
Log.d(TAG, "entering USB accessory mode");
|
|
mCurrentAccessory = new UsbAccessory(strings);
|
|
Intent intent = new Intent(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
|
|
intent.putExtra(UsbManager.EXTRA_ACCESSORY, mCurrentAccessory);
|
|
mContext.sendBroadcast(intent);
|
|
} else {
|
|
Log.e(TAG, "nativeGetAccessoryStrings failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Listens for uevent messages from the kernel to monitor the USB state (device mode)
|
|
*/
|
|
private final UEventObserver mUEventObserver = new UEventObserver() {
|
|
@Override
|
|
public void onUEvent(UEventObserver.UEvent event) {
|
|
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
|
Slog.v(TAG, "USB UEVENT: " + event.toString());
|
|
}
|
|
|
|
synchronized (mLock) {
|
|
String name = event.get("SWITCH_NAME");
|
|
String state = event.get("SWITCH_STATE");
|
|
if (name != null && state != null) {
|
|
try {
|
|
int intState = Integer.parseInt(state);
|
|
if ("usb_connected".equals(name)) {
|
|
mConnected = intState;
|
|
// trigger an Intent broadcast
|
|
if (mSystemReady) {
|
|
// debounce disconnects
|
|
update(mConnected == 0);
|
|
}
|
|
} else if ("usb_configuration".equals(name)) {
|
|
mConfiguration = intState;
|
|
// trigger an Intent broadcast
|
|
if (mSystemReady) {
|
|
update(mConnected == 0);
|
|
}
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
Slog.e(TAG, "Could not parse switch state from event " + event);
|
|
}
|
|
} else {
|
|
String function = event.get("FUNCTION");
|
|
String enabledStr = event.get("ENABLED");
|
|
if (function != null && enabledStr != null) {
|
|
// Note: we do not broadcast a change when a function is enabled or disabled.
|
|
// We just record the state change for the next broadcast.
|
|
int what = ("1".equals(enabledStr) ?
|
|
MSG_FUNCTION_ENABLED : MSG_FUNCTION_DISABLED);
|
|
Message msg = Message.obtain(mHandler, what);
|
|
msg.obj = function;
|
|
mHandler.sendMessage(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
public UsbService(Context context) {
|
|
mContext = context;
|
|
mHostBlacklist = context.getResources().getStringArray(
|
|
com.android.internal.R.array.config_usbHostBlacklist);
|
|
|
|
init(); // set initial status
|
|
|
|
if (mConfiguration >= 0) {
|
|
mUEventObserver.startObserving(USB_CONNECTED_MATCH);
|
|
mUEventObserver.startObserving(USB_CONFIGURATION_MATCH);
|
|
mUEventObserver.startObserving(USB_FUNCTIONS_MATCH);
|
|
}
|
|
}
|
|
|
|
private final void init() {
|
|
char[] buffer = new char[1024];
|
|
|
|
// Read initial USB state (device mode)
|
|
mConfiguration = -1;
|
|
try {
|
|
FileReader file = new FileReader(USB_CONNECTED_PATH);
|
|
int len = file.read(buffer, 0, 1024);
|
|
file.close();
|
|
mConnected = Integer.valueOf((new String(buffer, 0, len)).trim());
|
|
|
|
file = new FileReader(USB_CONFIGURATION_PATH);
|
|
len = file.read(buffer, 0, 1024);
|
|
file.close();
|
|
mConfiguration = Integer.valueOf((new String(buffer, 0, len)).trim());
|
|
|
|
} catch (FileNotFoundException e) {
|
|
Slog.i(TAG, "This kernel does not have USB configuration switch support");
|
|
} catch (Exception e) {
|
|
Slog.e(TAG, "" , e);
|
|
}
|
|
if (mConfiguration < 0)
|
|
return;
|
|
|
|
// Read initial list of enabled and disabled functions (device mode)
|
|
try {
|
|
File[] files = new File(USB_COMPOSITE_CLASS_PATH).listFiles();
|
|
for (int i = 0; i < files.length; i++) {
|
|
File file = new File(files[i], "enable");
|
|
FileReader reader = new FileReader(file);
|
|
int len = reader.read(buffer, 0, 1024);
|
|
reader.close();
|
|
int value = Integer.valueOf((new String(buffer, 0, len)).trim());
|
|
String functionName = files[i].getName();
|
|
if (value == 1) {
|
|
mEnabledFunctions.add(functionName);
|
|
} else {
|
|
mDisabledFunctions.add(functionName);
|
|
}
|
|
}
|
|
} catch (FileNotFoundException e) {
|
|
Slog.w(TAG, "This kernel does not have USB composite class support");
|
|
} catch (Exception e) {
|
|
Slog.e(TAG, "" , e);
|
|
}
|
|
}
|
|
|
|
private boolean isBlackListed(String deviceName) {
|
|
int count = mHostBlacklist.length;
|
|
for (int i = 0; i < count; i++) {
|
|
if (deviceName.startsWith(mHostBlacklist[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* returns true if the USB device should not be accessible by applications (host mode) */
|
|
private boolean isBlackListed(int clazz, int subClass, int protocol) {
|
|
// blacklist hubs
|
|
if (clazz == UsbConstants.USB_CLASS_HUB) return true;
|
|
|
|
// blacklist HID boot devices (mouse and keyboard)
|
|
if (clazz == UsbConstants.USB_CLASS_HID &&
|
|
subClass == UsbConstants.USB_INTERFACE_SUBCLASS_BOOT) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Called from JNI in monitorUsbHostBus() to report new USB devices (host mode) */
|
|
private void usbDeviceAdded(String deviceName, int vendorID, int productID,
|
|
int deviceClass, int deviceSubclass, int deviceProtocol,
|
|
/* array of quintuples containing id, class, subclass, protocol
|
|
and number of endpoints for each interface */
|
|
int[] interfaceValues,
|
|
/* array of quadruples containing address, attributes, max packet size
|
|
and interval for each endpoint */
|
|
int[] endpointValues) {
|
|
|
|
if (isBlackListed(deviceName) ||
|
|
isBlackListed(deviceClass, deviceSubclass, deviceProtocol)) {
|
|
return;
|
|
}
|
|
|
|
synchronized (mLock) {
|
|
if (mDevices.get(deviceName) != null) {
|
|
Log.w(TAG, "device already on mDevices list: " + deviceName);
|
|
return;
|
|
}
|
|
|
|
int numInterfaces = interfaceValues.length / 5;
|
|
Parcelable[] interfaces = new UsbInterface[numInterfaces];
|
|
try {
|
|
// repackage interfaceValues as an array of UsbInterface
|
|
int intf, endp, ival = 0, eval = 0;
|
|
for (intf = 0; intf < numInterfaces; intf++) {
|
|
int interfaceId = interfaceValues[ival++];
|
|
int interfaceClass = interfaceValues[ival++];
|
|
int interfaceSubclass = interfaceValues[ival++];
|
|
int interfaceProtocol = interfaceValues[ival++];
|
|
int numEndpoints = interfaceValues[ival++];
|
|
|
|
Parcelable[] endpoints = new UsbEndpoint[numEndpoints];
|
|
for (endp = 0; endp < numEndpoints; endp++) {
|
|
int address = endpointValues[eval++];
|
|
int attributes = endpointValues[eval++];
|
|
int maxPacketSize = endpointValues[eval++];
|
|
int interval = endpointValues[eval++];
|
|
endpoints[endp] = new UsbEndpoint(address, attributes,
|
|
maxPacketSize, interval);
|
|
}
|
|
|
|
// don't allow if any interfaces are blacklisted
|
|
if (isBlackListed(interfaceClass, interfaceSubclass, interfaceProtocol)) {
|
|
return;
|
|
}
|
|
interfaces[intf] = new UsbInterface(interfaceId, interfaceClass,
|
|
interfaceSubclass, interfaceProtocol, endpoints);
|
|
}
|
|
} catch (Exception e) {
|
|
// beware of index out of bound exceptions, which might happen if
|
|
// a device does not set bNumEndpoints correctly
|
|
Log.e(TAG, "error parsing USB descriptors", e);
|
|
return;
|
|
}
|
|
|
|
UsbDevice device = new UsbDevice(deviceName, vendorID, productID,
|
|
deviceClass, deviceSubclass, deviceProtocol, interfaces);
|
|
mDevices.put(deviceName, device);
|
|
|
|
Intent intent = new Intent(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
|
intent.putExtra(UsbManager.EXTRA_DEVICE, device);
|
|
Log.d(TAG, "usbDeviceAdded, sending " + intent);
|
|
mContext.sendBroadcast(intent);
|
|
}
|
|
}
|
|
|
|
/* Called from JNI in monitorUsbHostBus to report USB device removal (host mode) */
|
|
private void usbDeviceRemoved(String deviceName) {
|
|
synchronized (mLock) {
|
|
UsbDevice device = mDevices.remove(deviceName);
|
|
if (device != null) {
|
|
Intent intent = new Intent(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
|
intent.putExtra(UsbManager.EXTRA_DEVICE, device);
|
|
Log.d(TAG, "usbDeviceRemoved, sending " + intent);
|
|
mContext.sendBroadcast(intent);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void initHostSupport() {
|
|
// Create a thread to call into native code to wait for USB host events.
|
|
// This thread will call us back on usbDeviceAdded and usbDeviceRemoved.
|
|
Runnable runnable = new Runnable() {
|
|
public void run() {
|
|
monitorUsbHostBus();
|
|
}
|
|
};
|
|
new Thread(null, runnable, "UsbService host thread").start();
|
|
}
|
|
|
|
void systemReady() {
|
|
synchronized (mLock) {
|
|
if (mContext.getResources().getBoolean(
|
|
com.android.internal.R.bool.config_hasUsbHostSupport)) {
|
|
// start monitoring for connected USB devices
|
|
initHostSupport();
|
|
}
|
|
|
|
update(false);
|
|
mSystemReady = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Sends a message to update the USB connected and configured state (device mode).
|
|
* If delayed is true, then we add a small delay in sending the message to debounce
|
|
* the USB connection when enabling USB tethering.
|
|
*/
|
|
private final void update(boolean delayed) {
|
|
mHandler.removeMessages(MSG_UPDATE_STATE);
|
|
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_STATE, delayed ? UPDATE_DELAY : 0);
|
|
}
|
|
|
|
/* Returns a list of all currently attached USB devices (host mdoe) */
|
|
public void getDeviceList(Bundle devices) {
|
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_USB, null);
|
|
synchronized (mLock) {
|
|
for (String name : mDevices.keySet()) {
|
|
devices.putParcelable(name, mDevices.get(name));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Opens the specified USB device (host mode) */
|
|
public ParcelFileDescriptor openDevice(String deviceName) {
|
|
if (isBlackListed(deviceName)) {
|
|
throw new SecurityException("USB device is on a restricted bus");
|
|
}
|
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_USB, null);
|
|
if (mDevices.get(deviceName) == null) {
|
|
// if it is not in mDevices, it either does not exist or is blacklisted
|
|
throw new IllegalArgumentException(
|
|
"device " + deviceName + " does not exist or is restricted");
|
|
}
|
|
return nativeOpenDevice(deviceName);
|
|
}
|
|
|
|
/* returns the currently attached USB accessory (device mode) */
|
|
public UsbAccessory getCurrentAccessory() {
|
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_USB, null);
|
|
return mCurrentAccessory;
|
|
}
|
|
|
|
/* opens the currently attached USB accessory (device mode) */
|
|
public ParcelFileDescriptor openAccessory() {
|
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_USB, null);
|
|
return nativeOpenAccessory();
|
|
}
|
|
|
|
/*
|
|
* This handler is for deferred handling of events related to device mode and accessories.
|
|
*/
|
|
private final Handler mHandler = new Handler() {
|
|
private void addEnabledFunctionsLocked(Intent intent) {
|
|
// include state of all USB functions in our extras
|
|
for (int i = 0; i < mEnabledFunctions.size(); i++) {
|
|
intent.putExtra(mEnabledFunctions.get(i), UsbManager.USB_FUNCTION_ENABLED);
|
|
}
|
|
for (int i = 0; i < mDisabledFunctions.size(); i++) {
|
|
intent.putExtra(mDisabledFunctions.get(i), UsbManager.USB_FUNCTION_DISABLED);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
synchronized (mLock) {
|
|
switch (msg.what) {
|
|
case MSG_UPDATE_STATE:
|
|
if (mConnected != mLastConnected || mConfiguration != mLastConfiguration) {
|
|
if (mConnected == 0 && mCurrentAccessory != null) {
|
|
// turn off accessory mode when we are disconnected
|
|
if (UsbManager.setFunctionEnabled(
|
|
UsbManager.USB_FUNCTION_ACCESSORY, false)) {
|
|
Log.d(TAG, "exited USB accessory mode");
|
|
|
|
// restore previously enabled functions
|
|
for (String function : mAccessoryRestoreFunctions) {
|
|
if (UsbManager.setFunctionEnabled(function, true)) {
|
|
Log.e(TAG, "could not reenable function " + function);
|
|
}
|
|
}
|
|
mAccessoryRestoreFunctions.clear();
|
|
|
|
Intent intent = new Intent(
|
|
UsbManager.ACTION_USB_ACCESSORY_DETACHED);
|
|
intent.putExtra(UsbManager.EXTRA_ACCESSORY, mCurrentAccessory);
|
|
mContext.sendBroadcast(intent);
|
|
mCurrentAccessory = null;
|
|
|
|
// this will cause an immediate reset of the USB bus,
|
|
// so there is no point in sending the
|
|
// function disabled broadcast.
|
|
return;
|
|
} else {
|
|
Log.e(TAG, "could not disable USB_FUNCTION_ACCESSORY");
|
|
}
|
|
}
|
|
|
|
final ContentResolver cr = mContext.getContentResolver();
|
|
if (Settings.Secure.getInt(cr,
|
|
Settings.Secure.DEVICE_PROVISIONED, 0) == 0) {
|
|
Slog.i(TAG, "Device not provisioned, skipping USB broadcast");
|
|
return;
|
|
}
|
|
|
|
mLastConnected = mConnected;
|
|
mLastConfiguration = mConfiguration;
|
|
|
|
// send a sticky broadcast containing current USB state
|
|
Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
|
|
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
|
|
intent.putExtra(UsbManager.USB_CONNECTED, mConnected != 0);
|
|
intent.putExtra(UsbManager.USB_CONFIGURATION, mConfiguration);
|
|
addEnabledFunctionsLocked(intent);
|
|
mContext.sendStickyBroadcast(intent);
|
|
}
|
|
break;
|
|
case MSG_FUNCTION_ENABLED:
|
|
case MSG_FUNCTION_DISABLED:
|
|
functionEnabledLocked((String)msg.obj, msg.what == MSG_FUNCTION_ENABLED);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// host support
|
|
private native void monitorUsbHostBus();
|
|
private native ParcelFileDescriptor nativeOpenDevice(String deviceName);
|
|
// accessory support
|
|
private native String[] nativeGetAccessoryStrings();
|
|
private native ParcelFileDescriptor nativeOpenAccessory();
|
|
}
|