blob: 5239d976e66f659e4b1620a89b4ad1ec86cd609c [file] [log] [blame]
/*
* 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, "----------------");
}
}
}