1 /* 2 * Copyright (C) 2014 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 and 14 * limitations under the License. 15 */ 16 17 package com.android.server.usb; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.content.res.Resources; 22 import android.hardware.usb.UsbDevice; 23 import android.media.IAudioService; 24 import android.media.midi.MidiDeviceInfo; 25 import android.os.Bundle; 26 import android.os.ServiceManager; 27 import android.provider.Settings; 28 import android.service.usb.UsbAlsaManagerProto; 29 import android.util.Slog; 30 31 import com.android.internal.alsa.AlsaCardsParser; 32 import com.android.internal.util.dump.DualDumpOutputStream; 33 import com.android.server.usb.descriptors.UsbDescriptorParser; 34 35 import libcore.io.IoUtils; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.HashMap; 40 import java.util.List; 41 42 /** 43 * UsbAlsaManager manages USB audio and MIDI devices. 44 */ 45 public final class UsbAlsaManager { 46 private static final String TAG = UsbAlsaManager.class.getSimpleName(); 47 private static final boolean DEBUG = false; 48 49 // Flag to turn on/off multi-peripheral select mode 50 // Set to true to have single-device-only mode 51 private static final boolean mIsSingleMode = true; 52 53 private static final String ALSA_DIRECTORY = "/dev/snd/"; 54 55 private final Context mContext; 56 private IAudioService mAudioService; 57 private final boolean mHasMidiFeature; 58 59 private final AlsaCardsParser mCardsParser = new AlsaCardsParser(); 60 61 // this is needed to map USB devices to ALSA Audio Devices, especially to remove an 62 // ALSA device when we are notified that its associated USB device has been removed. 63 private final ArrayList<UsbAlsaDevice> mAlsaDevices = new ArrayList<UsbAlsaDevice>(); 64 private UsbAlsaDevice mSelectedDevice; 65 66 // 67 // Device Denylist 68 // 69 // This exists due to problems with Sony game controllers which present as an audio device 70 // even if no headset is connected and have no way to set the volume on the unit. 71 // Handle this by simply declining to use them as an audio device. 72 private static final int USB_VENDORID_SONY = 0x054C; 73 private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT1 = 0x05C4; 74 private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT2 = 0x09CC; 75 76 private static final int USB_DENYLIST_OUTPUT = 0x0001; 77 private static final int USB_DENYLIST_INPUT = 0x0002; 78 79 private static class DenyListEntry { 80 final int mVendorId; 81 final int mProductId; 82 final int mFlags; 83 DenyListEntry(int vendorId, int productId, int flags)84 DenyListEntry(int vendorId, int productId, int flags) { 85 mVendorId = vendorId; 86 mProductId = productId; 87 mFlags = flags; 88 } 89 } 90 91 static final List<DenyListEntry> sDeviceDenylist = Arrays.asList( 92 new DenyListEntry(USB_VENDORID_SONY, 93 USB_PRODUCTID_PS4CONTROLLER_ZCT1, 94 USB_DENYLIST_OUTPUT), 95 new DenyListEntry(USB_VENDORID_SONY, 96 USB_PRODUCTID_PS4CONTROLLER_ZCT2, 97 USB_DENYLIST_OUTPUT)); 98 isDeviceDenylisted(int vendorId, int productId, int flags)99 private static boolean isDeviceDenylisted(int vendorId, int productId, int flags) { 100 for (DenyListEntry entry : sDeviceDenylist) { 101 if (entry.mVendorId == vendorId && entry.mProductId == productId) { 102 // see if the type flag is set 103 return (entry.mFlags & flags) != 0; 104 } 105 } 106 107 return false; 108 } 109 110 /** 111 * List of connected MIDI devices 112 */ 113 private final HashMap<String, UsbMidiDevice> 114 mMidiDevices = new HashMap<String, UsbMidiDevice>(); 115 116 // UsbMidiDevice for USB peripheral mode (gadget) device 117 private UsbMidiDevice mPeripheralMidiDevice = null; 118 UsbAlsaManager(Context context)119 /* package */ UsbAlsaManager(Context context) { 120 mContext = context; 121 mHasMidiFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI); 122 } 123 systemReady()124 public void systemReady() { 125 mAudioService = IAudioService.Stub.asInterface( 126 ServiceManager.getService(Context.AUDIO_SERVICE)); 127 } 128 129 /** 130 * Select the AlsaDevice to be used for AudioService. 131 * AlsaDevice.start() notifies AudioService of it's connected state. 132 * 133 * @param alsaDevice The selected UsbAlsaDevice for system USB audio. 134 */ selectAlsaDevice(UsbAlsaDevice alsaDevice)135 private synchronized void selectAlsaDevice(UsbAlsaDevice alsaDevice) { 136 if (DEBUG) { 137 Slog.d(TAG, "selectAlsaDevice() " + alsaDevice); 138 } 139 140 // This must be where an existing USB audio device is deselected.... (I think) 141 if (mIsSingleMode && mSelectedDevice != null) { 142 deselectAlsaDevice(); 143 } 144 145 // FIXME Does not yet handle the case where the setting is changed 146 // after device connection. Ideally we should handle the settings change 147 // in SettingsObserver. Here we should log that a USB device is connected 148 // and disconnected with its address (card , device) and force the 149 // connection or disconnection when the setting changes. 150 int isDisabled = Settings.Secure.getInt(mContext.getContentResolver(), 151 Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0); 152 if (isDisabled != 0) { 153 return; 154 } 155 156 mSelectedDevice = alsaDevice; 157 alsaDevice.start(); 158 if (DEBUG) { 159 Slog.d(TAG, "selectAlsaDevice() - done."); 160 } 161 } 162 deselectAlsaDevice()163 private synchronized void deselectAlsaDevice() { 164 if (DEBUG) { 165 Slog.d(TAG, "deselectAlsaDevice() mSelectedDevice " + mSelectedDevice); 166 } 167 if (mSelectedDevice != null) { 168 mSelectedDevice.stop(); 169 mSelectedDevice = null; 170 } 171 } 172 getAlsaDeviceListIndexFor(String deviceAddress)173 private int getAlsaDeviceListIndexFor(String deviceAddress) { 174 for (int index = 0; index < mAlsaDevices.size(); index++) { 175 if (mAlsaDevices.get(index).getDeviceAddress().equals(deviceAddress)) { 176 return index; 177 } 178 } 179 return -1; 180 } 181 removeAlsaDeviceFromList(String deviceAddress)182 private UsbAlsaDevice removeAlsaDeviceFromList(String deviceAddress) { 183 int index = getAlsaDeviceListIndexFor(deviceAddress); 184 if (index > -1) { 185 return mAlsaDevices.remove(index); 186 } else { 187 return null; 188 } 189 } 190 selectDefaultDevice()191 /* package */ UsbAlsaDevice selectDefaultDevice() { 192 if (DEBUG) { 193 Slog.d(TAG, "selectDefaultDevice()"); 194 } 195 196 if (mAlsaDevices.size() > 0) { 197 UsbAlsaDevice alsaDevice = mAlsaDevices.get(0); 198 if (DEBUG) { 199 Slog.d(TAG, " alsaDevice:" + alsaDevice); 200 } 201 if (alsaDevice != null) { 202 selectAlsaDevice(alsaDevice); 203 } 204 return alsaDevice; 205 } else { 206 return null; 207 } 208 } 209 usbDeviceAdded(String deviceAddress, UsbDevice usbDevice, UsbDescriptorParser parser)210 /* package */ void usbDeviceAdded(String deviceAddress, UsbDevice usbDevice, 211 UsbDescriptorParser parser) { 212 if (DEBUG) { 213 Slog.d(TAG, "usbDeviceAdded(): " + usbDevice.getManufacturerName() 214 + " nm:" + usbDevice.getProductName()); 215 } 216 217 // Scan the Alsa File Space 218 mCardsParser.scan(); 219 220 // Find the ALSA spec for this device address 221 AlsaCardsParser.AlsaCardRecord cardRec = 222 mCardsParser.findCardNumFor(deviceAddress); 223 if (cardRec == null) { 224 return; 225 } 226 227 // Add it to the devices list 228 boolean hasInput = parser.hasInput() 229 && !isDeviceDenylisted(usbDevice.getVendorId(), usbDevice.getProductId(), 230 USB_DENYLIST_INPUT); 231 boolean hasOutput = parser.hasOutput() 232 && !isDeviceDenylisted(usbDevice.getVendorId(), usbDevice.getProductId(), 233 USB_DENYLIST_OUTPUT); 234 if (DEBUG) { 235 Slog.d(TAG, "hasInput: " + hasInput + " hasOutput:" + hasOutput); 236 } 237 if (hasInput || hasOutput) { 238 boolean isInputHeadset = parser.isInputHeadset(); 239 boolean isOutputHeadset = parser.isOutputHeadset(); 240 241 if (mAudioService == null) { 242 Slog.e(TAG, "no AudioService"); 243 return; 244 } 245 246 UsbAlsaDevice alsaDevice = 247 new UsbAlsaDevice(mAudioService, cardRec.getCardNum(), 0 /*device*/, 248 deviceAddress, hasOutput, hasInput, 249 isInputHeadset, isOutputHeadset); 250 if (alsaDevice != null) { 251 alsaDevice.setDeviceNameAndDescription( 252 cardRec.getCardName(), cardRec.getCardDescription()); 253 mAlsaDevices.add(0, alsaDevice); 254 selectAlsaDevice(alsaDevice); 255 } 256 } 257 258 // look for MIDI devices 259 boolean hasMidi = parser.hasMIDIInterface(); 260 if (DEBUG) { 261 Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature); 262 } 263 if (hasMidi && mHasMidiFeature) { 264 int device = 0; 265 Bundle properties = new Bundle(); 266 String manufacturer = usbDevice.getManufacturerName(); 267 String product = usbDevice.getProductName(); 268 String version = usbDevice.getVersion(); 269 String name; 270 if (manufacturer == null || manufacturer.isEmpty()) { 271 name = product; 272 } else if (product == null || product.isEmpty()) { 273 name = manufacturer; 274 } else { 275 name = manufacturer + " " + product; 276 } 277 properties.putString(MidiDeviceInfo.PROPERTY_NAME, name); 278 properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer); 279 properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product); 280 properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version); 281 properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER, 282 usbDevice.getSerialNumber()); 283 properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum()); 284 properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/); 285 properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice); 286 287 UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties, 288 cardRec.getCardNum(), 0 /*device*/); 289 if (usbMidiDevice != null) { 290 mMidiDevices.put(deviceAddress, usbMidiDevice); 291 } 292 } 293 294 logDevices("deviceAdded()"); 295 296 if (DEBUG) { 297 Slog.d(TAG, "deviceAdded() - done"); 298 } 299 } 300 usbDeviceRemoved(String deviceAddress )301 /* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) { 302 if (DEBUG) { 303 Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")"); 304 } 305 306 // Audio 307 UsbAlsaDevice alsaDevice = removeAlsaDeviceFromList(deviceAddress); 308 Slog.i(TAG, "USB Audio Device Removed: " + alsaDevice); 309 if (alsaDevice != null && alsaDevice == mSelectedDevice) { 310 deselectAlsaDevice(); 311 selectDefaultDevice(); // if there any external devices left, select one of them 312 } 313 314 // MIDI 315 UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress); 316 if (usbMidiDevice != null) { 317 Slog.i(TAG, "USB MIDI Device Removed: " + usbMidiDevice); 318 IoUtils.closeQuietly(usbMidiDevice); 319 } 320 321 logDevices("usbDeviceRemoved()"); 322 323 } 324 setPeripheralMidiState(boolean enabled, int card, int device)325 /* package */ void setPeripheralMidiState(boolean enabled, int card, int device) { 326 if (!mHasMidiFeature) { 327 return; 328 } 329 330 if (enabled && mPeripheralMidiDevice == null) { 331 Bundle properties = new Bundle(); 332 Resources r = mContext.getResources(); 333 properties.putString(MidiDeviceInfo.PROPERTY_NAME, r.getString( 334 com.android.internal.R.string.usb_midi_peripheral_name)); 335 properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, r.getString( 336 com.android.internal.R.string.usb_midi_peripheral_manufacturer_name)); 337 properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, r.getString( 338 com.android.internal.R.string.usb_midi_peripheral_product_name)); 339 properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card); 340 properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device); 341 mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device); 342 } else if (!enabled && mPeripheralMidiDevice != null) { 343 IoUtils.closeQuietly(mPeripheralMidiDevice); 344 mPeripheralMidiDevice = null; 345 } 346 } 347 348 // 349 // Devices List 350 // 351 /* 352 //import java.util.ArrayList; 353 public ArrayList<UsbAudioDevice> getConnectedDevices() { 354 ArrayList<UsbAudioDevice> devices = new ArrayList<UsbAudioDevice>(mAudioDevices.size()); 355 for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) { 356 devices.add(entry.getValue()); 357 } 358 return devices; 359 } 360 */ 361 362 /** 363 * Dump the USB alsa state. 364 */ 365 // invoked with "adb shell dumpsys usb" dump(DualDumpOutputStream dump, String idName, long id)366 public void dump(DualDumpOutputStream dump, String idName, long id) { 367 long token = dump.start(idName, id); 368 369 dump.write("cards_parser", UsbAlsaManagerProto.CARDS_PARSER, mCardsParser.getScanStatus()); 370 371 for (UsbAlsaDevice usbAlsaDevice : mAlsaDevices) { 372 usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES); 373 } 374 375 for (String deviceAddr : mMidiDevices.keySet()) { 376 // A UsbMidiDevice does not have a handle to the UsbDevice anymore 377 mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices", 378 UsbAlsaManagerProto.MIDI_DEVICES); 379 } 380 381 dump.end(token); 382 } 383 logDevicesList(String title)384 public void logDevicesList(String title) { 385 if (DEBUG) { 386 Slog.i(TAG, title + "----------------"); 387 for (UsbAlsaDevice alsaDevice : mAlsaDevices) { 388 Slog.i(TAG, " -->"); 389 Slog.i(TAG, "" + alsaDevice); 390 Slog.i(TAG, " <--"); 391 } 392 Slog.i(TAG, "----------------"); 393 } 394 } 395 396 // This logs a more terse (and more readable) version of the devices list logDevices(String title)397 public void logDevices(String title) { 398 if (DEBUG) { 399 Slog.i(TAG, title + "----------------"); 400 for (UsbAlsaDevice alsaDevice : mAlsaDevices) { 401 Slog.i(TAG, alsaDevice.toShortString()); 402 } 403 Slog.i(TAG, "----------------"); 404 } 405 } 406 } 407