1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions an 14 * limitations under the License. 15 */ 16 17package com.android.server.usb; 18 19import android.alsa.AlsaCardsParser; 20import android.alsa.AlsaDevicesParser; 21import android.content.Context; 22import android.content.Intent; 23import android.hardware.usb.UsbConfiguration; 24import android.hardware.usb.UsbConstants; 25import android.hardware.usb.UsbDevice; 26import android.hardware.usb.UsbEndpoint; 27import android.hardware.usb.UsbInterface; 28import android.media.AudioManager; 29import android.os.Bundle; 30import android.os.ParcelFileDescriptor; 31import android.os.Parcelable; 32import android.os.UserHandle; 33import android.util.Slog; 34 35import com.android.internal.annotations.GuardedBy; 36 37import java.io.File; 38import java.io.FileDescriptor; 39import java.io.FileNotFoundException; 40import java.io.PrintWriter; 41import java.util.ArrayList; 42import java.util.HashMap; 43 44/** 45 * UsbHostManager manages USB state in host mode. 46 */ 47public class UsbHostManager { 48 private static final String TAG = UsbHostManager.class.getSimpleName(); 49 private static final boolean DEBUG_AUDIO = false; 50 51 // contains all connected USB devices 52 private final HashMap<String, UsbDevice> mDevices = new HashMap<String, UsbDevice>(); 53 54 // USB busses to exclude from USB host support 55 private final String[] mHostBlacklist; 56 57 private final Context mContext; 58 private final Object mLock = new Object(); 59 60 private UsbDevice mNewDevice; 61 private UsbConfiguration mNewConfiguration; 62 private UsbInterface mNewInterface; 63 private ArrayList<UsbConfiguration> mNewConfigurations; 64 private ArrayList<UsbInterface> mNewInterfaces; 65 private ArrayList<UsbEndpoint> mNewEndpoints; 66 67 // Attributes of any connected USB audio device. 68 //TODO(pmclean) When we extend to multiple, USB Audio devices, we will need to get 69 // more clever about this. 70 private int mConnectedUsbCard = -1; 71 private int mConnectedUsbDeviceNum = -1; 72 private boolean mConnectedHasPlayback = false; 73 private boolean mConnectedHasCapture = false; 74 private boolean mConnectedHasMIDI = false; 75 76 @GuardedBy("mLock") 77 private UsbSettingsManager mCurrentSettings; 78 79 public UsbHostManager(Context context) { 80 mContext = context; 81 mHostBlacklist = context.getResources().getStringArray( 82 com.android.internal.R.array.config_usbHostBlacklist); 83 } 84 85 public void setCurrentSettings(UsbSettingsManager settings) { 86 synchronized (mLock) { 87 mCurrentSettings = settings; 88 } 89 } 90 91 private UsbSettingsManager getCurrentSettings() { 92 synchronized (mLock) { 93 return mCurrentSettings; 94 } 95 } 96 97 private boolean isBlackListed(String deviceName) { 98 int count = mHostBlacklist.length; 99 for (int i = 0; i < count; i++) { 100 if (deviceName.startsWith(mHostBlacklist[i])) { 101 return true; 102 } 103 } 104 return false; 105 } 106 107 /* returns true if the USB device should not be accessible by applications */ 108 private boolean isBlackListed(int clazz, int subClass, int protocol) { 109 // blacklist hubs 110 if (clazz == UsbConstants.USB_CLASS_HUB) return true; 111 112 // blacklist HID boot devices (mouse and keyboard) 113 if (clazz == UsbConstants.USB_CLASS_HID && 114 subClass == UsbConstants.USB_INTERFACE_SUBCLASS_BOOT) { 115 return true; 116 } 117 118 return false; 119 } 120 121 // Broadcasts the arrival/departure of a USB audio interface 122 // card - the ALSA card number of the physical interface 123 // device - the ALSA device number of the physical interface 124 // enabled - if true, we're connecting a device (it's arrived), else disconnecting 125 private void sendDeviceNotification(int card, int device, boolean enabled, 126 boolean hasPlayback, boolean hasCapture, boolean hasMIDI) { 127 // send a sticky broadcast containing current USB state 128 Intent intent = new Intent(AudioManager.ACTION_USB_AUDIO_DEVICE_PLUG); 129 intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); 130 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 131 intent.putExtra("state", enabled ? 1 : 0); 132 intent.putExtra("card", card); 133 intent.putExtra("device", device); 134 intent.putExtra("hasPlayback", hasPlayback); 135 intent.putExtra("hasCapture", hasCapture); 136 intent.putExtra("hasMIDI", hasMIDI); 137 mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); 138 } 139 140 private boolean waitForAlsaFile(int card, int device, boolean capture) { 141 // These values were empirically determined. 142 final int kNumRetries = 5; 143 final int kSleepTime = 500; // ms 144 String alsaDevPath = "/dev/snd/pcmC" + card + "D" + device + (capture ? "c" : "p"); 145 File alsaDevFile = new File(alsaDevPath); 146 boolean exists = false; 147 for (int retry = 0; !exists && retry < kNumRetries; retry++) { 148 exists = alsaDevFile.exists(); 149 if (!exists) { 150 try { 151 Thread.sleep(kSleepTime); 152 } catch (IllegalThreadStateException ex) { 153 Slog.d(TAG, "usb: IllegalThreadStateException while waiting for ALSA file."); 154 } catch (java.lang.InterruptedException ex) { 155 Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file."); 156 } 157 } 158 } 159 160 return exists; 161 } 162 163 /* Called from JNI in monitorUsbHostBus() to report new USB devices 164 Returns true if successful, in which case the JNI code will continue adding configurations, 165 interfaces and endpoints, and finally call endUsbDeviceAdded after all descriptors 166 have been processed 167 */ 168 private boolean beginUsbDeviceAdded(String deviceName, int vendorID, int productID, 169 int deviceClass, int deviceSubclass, int deviceProtocol, 170 String manufacturerName, String productName, String serialNumber) { 171 172 if (DEBUG_AUDIO) { 173 Slog.d(TAG, "usb:UsbHostManager.beginUsbDeviceAdded(" + deviceName + ")"); 174 // Audio Class Codes: 175 // Audio: 0x01 176 // Audio Subclass Codes: 177 // undefined: 0x00 178 // audio control: 0x01 179 // audio streaming: 0x02 180 // midi streaming: 0x03 181 182 // some useful debugging info 183 Slog.d(TAG, "usb: nm:" + deviceName + " vnd:" + vendorID + " prd:" + productID + " cls:" 184 + deviceClass + " sub:" + deviceSubclass + " proto:" + deviceProtocol); 185 } 186 187 // OK this is non-obvious, but true. One can't tell if the device being attached is even 188 // potentially an audio device without parsing the interface descriptors, so punt on any 189 // such test until endUsbDeviceAdded() when we have that info. 190 191 if (isBlackListed(deviceName) || 192 isBlackListed(deviceClass, deviceSubclass, deviceProtocol)) { 193 return false; 194 } 195 196 synchronized (mLock) { 197 if (mDevices.get(deviceName) != null) { 198 Slog.w(TAG, "device already on mDevices list: " + deviceName); 199 return false; 200 } 201 202 if (mNewDevice != null) { 203 Slog.e(TAG, "mNewDevice is not null in endUsbDeviceAdded"); 204 return false; 205 } 206 207 mNewDevice = new UsbDevice(deviceName, vendorID, productID, 208 deviceClass, deviceSubclass, deviceProtocol, 209 manufacturerName, productName, serialNumber); 210 211 mNewConfigurations = new ArrayList<UsbConfiguration>(); 212 mNewInterfaces = new ArrayList<UsbInterface>(); 213 mNewEndpoints = new ArrayList<UsbEndpoint>(); 214 } 215 216 return true; 217 } 218 219 /* Called from JNI in monitorUsbHostBus() to report new USB configuration for the device 220 currently being added. Returns true if successful, false in case of error. 221 */ 222 private void addUsbConfiguration(int id, String name, int attributes, int maxPower) { 223 if (mNewConfiguration != null) { 224 mNewConfiguration.setInterfaces( 225 mNewInterfaces.toArray(new UsbInterface[mNewInterfaces.size()])); 226 mNewInterfaces.clear(); 227 } 228 229 mNewConfiguration = new UsbConfiguration(id, name, attributes, maxPower); 230 mNewConfigurations.add(mNewConfiguration); 231 } 232 233 /* Called from JNI in monitorUsbHostBus() to report new USB interface for the device 234 currently being added. Returns true if successful, false in case of error. 235 */ 236 private void addUsbInterface(int id, String name, int altSetting, 237 int Class, int subClass, int protocol) { 238 if (mNewInterface != null) { 239 mNewInterface.setEndpoints( 240 mNewEndpoints.toArray(new UsbEndpoint[mNewEndpoints.size()])); 241 mNewEndpoints.clear(); 242 } 243 244 mNewInterface = new UsbInterface(id, altSetting, name, Class, subClass, protocol); 245 mNewInterfaces.add(mNewInterface); 246 } 247 248 /* Called from JNI in monitorUsbHostBus() to report new USB endpoint for the device 249 currently being added. Returns true if successful, false in case of error. 250 */ 251 private void addUsbEndpoint(int address, int attributes, int maxPacketSize, int interval) { 252 mNewEndpoints.add(new UsbEndpoint(address, attributes, maxPacketSize, interval)); 253 } 254 255 /* Called from JNI in monitorUsbHostBus() to finish adding a new device */ 256 private void endUsbDeviceAdded() { 257 if (DEBUG_AUDIO) { 258 Slog.d(TAG, "usb:UsbHostManager.endUsbDeviceAdded()"); 259 } 260 if (mNewInterface != null) { 261 mNewInterface.setEndpoints( 262 mNewEndpoints.toArray(new UsbEndpoint[mNewEndpoints.size()])); 263 } 264 if (mNewConfiguration != null) { 265 mNewConfiguration.setInterfaces( 266 mNewInterfaces.toArray(new UsbInterface[mNewInterfaces.size()])); 267 } 268 269 // Is there an audio interface in there? 270 final int kUsbClassId_Audio = 0x01; 271 boolean isAudioDevice = false; 272 for (int ntrfaceIndex = 0; !isAudioDevice && ntrfaceIndex < mNewInterfaces.size(); 273 ntrfaceIndex++) { 274 UsbInterface ntrface = mNewInterfaces.get(ntrfaceIndex); 275 if (ntrface.getInterfaceClass() == kUsbClassId_Audio) { 276 isAudioDevice = true; 277 } 278 } 279 280 synchronized (mLock) { 281 if (mNewDevice != null) { 282 mNewDevice.setConfigurations( 283 mNewConfigurations.toArray(new UsbConfiguration[mNewConfigurations.size()])); 284 mDevices.put(mNewDevice.getDeviceName(), mNewDevice); 285 Slog.d(TAG, "Added device " + mNewDevice); 286 getCurrentSettings().deviceAttached(mNewDevice); 287 } else { 288 Slog.e(TAG, "mNewDevice is null in endUsbDeviceAdded"); 289 } 290 mNewDevice = null; 291 mNewConfigurations = null; 292 mNewInterfaces = null; 293 mNewEndpoints = null; 294 } 295 296 if (!isAudioDevice) { 297 return; // bail 298 } 299 300 //TODO(pmclean) The "Parser" objects inspect files in "/proc/asound" which we presume is 301 // present, unlike the waitForAlsaFile() which waits on a file in /dev/snd. It is not 302 // clear why this works, or that it can be relied on going forward. Needs further 303 // research. 304 AlsaCardsParser cardsParser = new AlsaCardsParser(); 305 cardsParser.scan(); 306 // cardsParser.Log(); 307 308 // But we need to parse the device to determine its capabilities. 309 AlsaDevicesParser devicesParser = new AlsaDevicesParser(); 310 devicesParser.scan(); 311 // devicesParser.Log(); 312 313 // The protocol for now will be to select the last-connected (highest-numbered) 314 // Alsa Card. 315 mConnectedUsbCard = cardsParser.getNumCardRecords() - 1; 316 mConnectedUsbDeviceNum = 0; 317 318 mConnectedHasPlayback = devicesParser.hasPlaybackDevices(mConnectedUsbCard); 319 mConnectedHasCapture = devicesParser.hasCaptureDevices(mConnectedUsbCard); 320 mConnectedHasMIDI = devicesParser.hasMIDIDevices(mConnectedUsbCard); 321 322 // Playback device file needed/present? 323 if (mConnectedHasPlayback && 324 !waitForAlsaFile(mConnectedUsbCard, mConnectedUsbDeviceNum, false)) { 325 return; 326 } 327 328 // Capture device file needed/present? 329 if (mConnectedHasCapture && 330 !waitForAlsaFile(mConnectedUsbCard, mConnectedUsbDeviceNum, true)) { 331 return; 332 } 333 334 if (DEBUG_AUDIO) { 335 Slog.d(TAG, 336 "usb: hasPlayback:" + mConnectedHasPlayback + " hasCapture:" + mConnectedHasCapture); 337 } 338 339 sendDeviceNotification(mConnectedUsbCard, 340 mConnectedUsbDeviceNum, 341 true, 342 mConnectedHasPlayback, 343 mConnectedHasCapture, 344 mConnectedHasMIDI); 345 } 346 347 /* Called from JNI in monitorUsbHostBus to report USB device removal */ 348 private void usbDeviceRemoved(String deviceName) { 349 if (DEBUG_AUDIO) { 350 Slog.d(TAG, "usb:UsbHostManager.usbDeviceRemoved() nm:" + deviceName); 351 } 352 353 if (mConnectedUsbCard != -1 && mConnectedUsbDeviceNum != -1) { 354 sendDeviceNotification(mConnectedUsbCard, 355 mConnectedUsbDeviceNum, 356 false, 357 mConnectedHasPlayback, 358 mConnectedHasCapture, 359 mConnectedHasMIDI); 360 mConnectedUsbCard = -1; 361 mConnectedUsbDeviceNum = -1; 362 mConnectedHasPlayback = false; 363 mConnectedHasCapture = false; 364 mConnectedHasMIDI = false; 365 } 366 367 synchronized (mLock) { 368 UsbDevice device = mDevices.remove(deviceName); 369 if (device != null) { 370 getCurrentSettings().deviceDetached(device); 371 } 372 } 373 } 374 375 public void systemReady() { 376 synchronized (mLock) { 377 // Create a thread to call into native code to wait for USB host events. 378 // This thread will call us back on usbDeviceAdded and usbDeviceRemoved. 379 Runnable runnable = new Runnable() { 380 public void run() { 381 monitorUsbHostBus(); 382 } 383 }; 384 new Thread(null, runnable, "UsbService host thread").start(); 385 } 386 } 387 388 /* Returns a list of all currently attached USB devices */ 389 public void getDeviceList(Bundle devices) { 390 synchronized (mLock) { 391 for (String name : mDevices.keySet()) { 392 devices.putParcelable(name, mDevices.get(name)); 393 } 394 } 395 } 396 397 /* Opens the specified USB device */ 398 public ParcelFileDescriptor openDevice(String deviceName) { 399 synchronized (mLock) { 400 if (isBlackListed(deviceName)) { 401 throw new SecurityException("USB device is on a restricted bus"); 402 } 403 UsbDevice device = mDevices.get(deviceName); 404 if (device == null) { 405 // if it is not in mDevices, it either does not exist or is blacklisted 406 throw new IllegalArgumentException( 407 "device " + deviceName + " does not exist or is restricted"); 408 } 409 getCurrentSettings().checkPermission(device); 410 return nativeOpenDevice(deviceName); 411 } 412 } 413 414 public void dump(FileDescriptor fd, PrintWriter pw) { 415 synchronized (mLock) { 416 pw.println(" USB Host State:"); 417 for (String name : mDevices.keySet()) { 418 pw.println(" " + name + ": " + mDevices.get(name)); 419 } 420 } 421 } 422 423 private native void monitorUsbHostBus(); 424 private native ParcelFileDescriptor nativeOpenDevice(String deviceName); 425} 426