| /* |
| * Copyright (C) 2014 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; |
| |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.hardware.usb.UsbDevice; |
| import android.media.IAudioService; |
| import android.media.midi.MidiDeviceInfo; |
| import android.os.Bundle; |
| import android.os.ServiceManager; |
| import android.provider.Settings; |
| import android.service.usb.UsbAlsaManagerProto; |
| import android.util.Slog; |
| |
| import com.android.internal.alsa.AlsaCardsParser; |
| import com.android.internal.util.dump.DualDumpOutputStream; |
| import com.android.server.usb.descriptors.UsbDescriptorParser; |
| |
| import libcore.io.IoUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * UsbAlsaManager manages USB audio and MIDI devices. |
| */ |
| public final class UsbAlsaManager { |
| private static final String TAG = UsbAlsaManager.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| |
| // Flag to turn on/off multi-peripheral select mode |
| // Set to true to have single-device-only mode |
| private static final boolean mIsSingleMode = true; |
| |
| private static final String ALSA_DIRECTORY = "/dev/snd/"; |
| |
| private final Context mContext; |
| private IAudioService mAudioService; |
| private final boolean mHasMidiFeature; |
| |
| private final AlsaCardsParser mCardsParser = new AlsaCardsParser(); |
| |
| // this is needed to map USB devices to ALSA Audio Devices, especially to remove an |
| // ALSA device when we are notified that its associated USB device has been removed. |
| private final ArrayList<UsbAlsaDevice> mAlsaDevices = new ArrayList<UsbAlsaDevice>(); |
| private UsbAlsaDevice mSelectedDevice; |
| |
| // |
| // Device Blacklist |
| // |
| // This exists due to problems with Sony game controllers which present as an audio device |
| // even if no headset is connected and have no way to set the volume on the unit. |
| // Handle this by simply declining to use them as an audio device. |
| private static final int USB_VENDORID_SONY = 0x054C; |
| private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT1 = 0x05C4; |
| private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT2 = 0x09CC; |
| |
| private static final int USB_BLACKLIST_OUTPUT = 0x0001; |
| private static final int USB_BLACKLIST_INPUT = 0x0002; |
| |
| private static class BlackListEntry { |
| final int mVendorId; |
| final int mProductId; |
| final int mFlags; |
| |
| BlackListEntry(int vendorId, int productId, int flags) { |
| mVendorId = vendorId; |
| mProductId = productId; |
| mFlags = flags; |
| } |
| } |
| |
| static final List<BlackListEntry> sDeviceBlacklist = Arrays.asList( |
| new BlackListEntry(USB_VENDORID_SONY, |
| USB_PRODUCTID_PS4CONTROLLER_ZCT1, |
| USB_BLACKLIST_OUTPUT), |
| new BlackListEntry(USB_VENDORID_SONY, |
| USB_PRODUCTID_PS4CONTROLLER_ZCT2, |
| USB_BLACKLIST_OUTPUT)); |
| |
| private static boolean isDeviceBlacklisted(int vendorId, int productId, int flags) { |
| for (BlackListEntry entry : sDeviceBlacklist) { |
| if (entry.mVendorId == vendorId && entry.mProductId == productId) { |
| // see if the type flag is set |
| return (entry.mFlags & flags) != 0; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * List of connected MIDI devices |
| */ |
| private final HashMap<String, UsbMidiDevice> |
| mMidiDevices = new HashMap<String, UsbMidiDevice>(); |
| |
| // UsbMidiDevice for USB peripheral mode (gadget) device |
| private UsbMidiDevice mPeripheralMidiDevice = null; |
| |
| /* package */ UsbAlsaManager(Context context) { |
| mContext = context; |
| mHasMidiFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI); |
| } |
| |
| public void systemReady() { |
| mAudioService = IAudioService.Stub.asInterface( |
| ServiceManager.getService(Context.AUDIO_SERVICE)); |
| } |
| |
| /** |
| * Select the AlsaDevice to be used for AudioService. |
| * AlsaDevice.start() notifies AudioService of it's connected state. |
| * |
| * @param alsaDevice The selected UsbAlsaDevice for system USB audio. |
| */ |
| private synchronized void selectAlsaDevice(UsbAlsaDevice alsaDevice) { |
| if (DEBUG) { |
| Slog.d(TAG, "selectAlsaDevice() " + alsaDevice); |
| } |
| |
| // This must be where an existing USB audio device is deselected.... (I think) |
| if (mIsSingleMode && mSelectedDevice != null) { |
| deselectAlsaDevice(); |
| } |
| |
| // FIXME Does not yet handle the case where the setting is changed |
| // after device connection. Ideally we should handle the settings change |
| // in SettingsObserver. Here we should log that a USB device is connected |
| // and disconnected with its address (card , device) and force the |
| // connection or disconnection when the setting changes. |
| int isDisabled = Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0); |
| if (isDisabled != 0) { |
| return; |
| } |
| |
| mSelectedDevice = alsaDevice; |
| alsaDevice.start(); |
| if (DEBUG) { |
| Slog.d(TAG, "selectAlsaDevice() - done."); |
| } |
| } |
| |
| private synchronized void deselectAlsaDevice() { |
| if (DEBUG) { |
| Slog.d(TAG, "deselectAlsaDevice() mSelectedDevice " + mSelectedDevice); |
| } |
| if (mSelectedDevice != null) { |
| mSelectedDevice.stop(); |
| mSelectedDevice = null; |
| } |
| } |
| |
| private int getAlsaDeviceListIndexFor(String deviceAddress) { |
| for (int index = 0; index < mAlsaDevices.size(); index++) { |
| if (mAlsaDevices.get(index).getDeviceAddress().equals(deviceAddress)) { |
| return index; |
| } |
| } |
| return -1; |
| } |
| |
| private UsbAlsaDevice removeAlsaDeviceFromList(String deviceAddress) { |
| int index = getAlsaDeviceListIndexFor(deviceAddress); |
| if (index > -1) { |
| return mAlsaDevices.remove(index); |
| } else { |
| return null; |
| } |
| } |
| |
| /* package */ UsbAlsaDevice selectDefaultDevice() { |
| if (DEBUG) { |
| Slog.d(TAG, "selectDefaultDevice()"); |
| } |
| |
| if (mAlsaDevices.size() > 0) { |
| UsbAlsaDevice alsaDevice = mAlsaDevices.get(0); |
| if (DEBUG) { |
| Slog.d(TAG, " alsaDevice:" + alsaDevice); |
| } |
| if (alsaDevice != null) { |
| selectAlsaDevice(alsaDevice); |
| } |
| return alsaDevice; |
| } else { |
| return null; |
| } |
| } |
| |
| /* package */ void usbDeviceAdded(String deviceAddress, UsbDevice usbDevice, |
| UsbDescriptorParser parser) { |
| if (DEBUG) { |
| Slog.d(TAG, "usbDeviceAdded(): " + usbDevice.getManufacturerName() |
| + " nm:" + usbDevice.getProductName()); |
| } |
| |
| // Scan the Alsa File Space |
| mCardsParser.scan(); |
| |
| // Find the ALSA spec for this device address |
| AlsaCardsParser.AlsaCardRecord cardRec = |
| mCardsParser.findCardNumFor(deviceAddress); |
| if (cardRec == null) { |
| return; |
| } |
| |
| // Add it to the devices list |
| boolean hasInput = parser.hasInput() |
| && !isDeviceBlacklisted(usbDevice.getVendorId(), usbDevice.getProductId(), |
| USB_BLACKLIST_INPUT); |
| boolean hasOutput = parser.hasOutput() |
| && !isDeviceBlacklisted(usbDevice.getVendorId(), usbDevice.getProductId(), |
| USB_BLACKLIST_OUTPUT); |
| if (DEBUG) { |
| Slog.d(TAG, "hasInput: " + hasInput + " hasOutput:" + hasOutput); |
| } |
| if (hasInput || hasOutput) { |
| boolean isInputHeadset = parser.isInputHeadset(); |
| boolean isOutputHeadset = parser.isOutputHeadset(); |
| |
| if (mAudioService == null) { |
| Slog.e(TAG, "no AudioService"); |
| return; |
| } |
| |
| UsbAlsaDevice alsaDevice = |
| new UsbAlsaDevice(mAudioService, cardRec.getCardNum(), 0 /*device*/, |
| deviceAddress, hasOutput, hasInput, |
| isInputHeadset, isOutputHeadset); |
| if (alsaDevice != null) { |
| alsaDevice.setDeviceNameAndDescription( |
| cardRec.getCardName(), cardRec.getCardDescription()); |
| mAlsaDevices.add(0, alsaDevice); |
| selectAlsaDevice(alsaDevice); |
| } |
| } |
| |
| // look for MIDI devices |
| boolean hasMidi = parser.hasMIDIInterface(); |
| if (DEBUG) { |
| Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature); |
| } |
| if (hasMidi && mHasMidiFeature) { |
| int device = 0; |
| Bundle properties = new Bundle(); |
| String manufacturer = usbDevice.getManufacturerName(); |
| String product = usbDevice.getProductName(); |
| String version = usbDevice.getVersion(); |
| String name; |
| if (manufacturer == null || manufacturer.isEmpty()) { |
| name = product; |
| } else if (product == null || product.isEmpty()) { |
| name = manufacturer; |
| } else { |
| name = manufacturer + " " + product; |
| } |
| properties.putString(MidiDeviceInfo.PROPERTY_NAME, name); |
| properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer); |
| properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product); |
| properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version); |
| properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER, |
| usbDevice.getSerialNumber()); |
| properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum()); |
| properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/); |
| properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice); |
| |
| UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties, |
| cardRec.getCardNum(), 0 /*device*/); |
| if (usbMidiDevice != null) { |
| mMidiDevices.put(deviceAddress, usbMidiDevice); |
| } |
| } |
| |
| logDevices("deviceAdded()"); |
| |
| if (DEBUG) { |
| Slog.d(TAG, "deviceAdded() - done"); |
| } |
| } |
| |
| /* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) { |
| if (DEBUG) { |
| Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")"); |
| } |
| |
| // Audio |
| UsbAlsaDevice alsaDevice = removeAlsaDeviceFromList(deviceAddress); |
| Slog.i(TAG, "USB Audio Device Removed: " + alsaDevice); |
| if (alsaDevice != null && alsaDevice == mSelectedDevice) { |
| deselectAlsaDevice(); |
| selectDefaultDevice(); // if there any external devices left, select one of them |
| } |
| |
| // MIDI |
| UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress); |
| if (usbMidiDevice != null) { |
| Slog.i(TAG, "USB MIDI Device Removed: " + usbMidiDevice); |
| IoUtils.closeQuietly(usbMidiDevice); |
| } |
| |
| logDevices("usbDeviceRemoved()"); |
| |
| } |
| |
| /* package */ void setPeripheralMidiState(boolean enabled, int card, int device) { |
| if (!mHasMidiFeature) { |
| return; |
| } |
| |
| if (enabled && mPeripheralMidiDevice == null) { |
| Bundle properties = new Bundle(); |
| Resources r = mContext.getResources(); |
| properties.putString(MidiDeviceInfo.PROPERTY_NAME, r.getString( |
| com.android.internal.R.string.usb_midi_peripheral_name)); |
| properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, r.getString( |
| com.android.internal.R.string.usb_midi_peripheral_manufacturer_name)); |
| properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, r.getString( |
| com.android.internal.R.string.usb_midi_peripheral_product_name)); |
| properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card); |
| properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device); |
| mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device); |
| } else if (!enabled && mPeripheralMidiDevice != null) { |
| IoUtils.closeQuietly(mPeripheralMidiDevice); |
| mPeripheralMidiDevice = null; |
| } |
| } |
| |
| // |
| // Devices List |
| // |
| /* |
| //import java.util.ArrayList; |
| public ArrayList<UsbAudioDevice> getConnectedDevices() { |
| ArrayList<UsbAudioDevice> devices = new ArrayList<UsbAudioDevice>(mAudioDevices.size()); |
| for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) { |
| devices.add(entry.getValue()); |
| } |
| return devices; |
| } |
| */ |
| |
| /** |
| * Dump the USB alsa state. |
| */ |
| // invoked with "adb shell dumpsys usb" |
| public void dump(DualDumpOutputStream dump, String idName, long id) { |
| long token = dump.start(idName, id); |
| |
| dump.write("cards_parser", UsbAlsaManagerProto.CARDS_PARSER, mCardsParser.getScanStatus()); |
| |
| for (UsbAlsaDevice usbAlsaDevice : mAlsaDevices) { |
| usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES); |
| } |
| |
| for (String deviceAddr : mMidiDevices.keySet()) { |
| // A UsbMidiDevice does not have a handle to the UsbDevice anymore |
| mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices", |
| UsbAlsaManagerProto.MIDI_DEVICES); |
| } |
| |
| dump.end(token); |
| } |
| |
| public void logDevicesList(String title) { |
| if (DEBUG) { |
| Slog.i(TAG, title + "----------------"); |
| for (UsbAlsaDevice alsaDevice : mAlsaDevices) { |
| Slog.i(TAG, " -->"); |
| Slog.i(TAG, "" + alsaDevice); |
| Slog.i(TAG, " <--"); |
| } |
| Slog.i(TAG, "----------------"); |
| } |
| } |
| |
| // This logs a more terse (and more readable) version of the devices list |
| public void logDevices(String title) { |
| if (DEBUG) { |
| Slog.i(TAG, title + "----------------"); |
| for (UsbAlsaDevice alsaDevice : mAlsaDevices) { |
| Slog.i(TAG, alsaDevice.toShortString()); |
| } |
| Slog.i(TAG, "----------------"); |
| } |
| } |
| } |