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 an
14 * limitations under the License.
15 */
16
17package com.android.server.usb;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.Resources;
22import android.hardware.usb.UsbConstants;
23import android.hardware.usb.UsbDevice;
24import android.hardware.usb.UsbInterface;
25import android.media.AudioSystem;
26import android.media.IAudioService;
27import android.media.midi.MidiDeviceInfo;
28import android.os.FileObserver;
29import android.os.Bundle;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.SystemClock;
33import android.provider.Settings;
34import android.util.Slog;
35
36import com.android.internal.alsa.AlsaCardsParser;
37import com.android.internal.alsa.AlsaDevicesParser;
38import com.android.internal.util.IndentingPrintWriter;
39import com.android.server.audio.AudioService;
40
41import libcore.io.IoUtils;
42
43import java.io.File;
44import java.io.FileDescriptor;
45import java.io.PrintWriter;
46import java.util.HashMap;
47import java.util.ArrayList;
48
49/**
50 * UsbAlsaManager manages USB audio and MIDI devices.
51 */
52public final class UsbAlsaManager {
53    private static final String TAG = UsbAlsaManager.class.getSimpleName();
54    private static final boolean DEBUG = false;
55
56    private static final String ALSA_DIRECTORY = "/dev/snd/";
57
58    private final Context mContext;
59    private IAudioService mAudioService;
60    private final boolean mHasMidiFeature;
61
62    private final AlsaCardsParser mCardsParser = new AlsaCardsParser();
63    private final AlsaDevicesParser mDevicesParser = new AlsaDevicesParser();
64
65    // this is needed to map USB devices to ALSA Audio Devices, especially to remove an
66    // ALSA device when we are notified that its associated USB device has been removed.
67
68    private final HashMap<UsbDevice,UsbAudioDevice>
69        mAudioDevices = new HashMap<UsbDevice,UsbAudioDevice>();
70
71    private final HashMap<UsbDevice,UsbMidiDevice>
72        mMidiDevices = new HashMap<UsbDevice,UsbMidiDevice>();
73
74    private final HashMap<String,AlsaDevice>
75        mAlsaDevices = new HashMap<String,AlsaDevice>();
76
77    private UsbAudioDevice mAccessoryAudioDevice = null;
78
79    // UsbMidiDevice for USB peripheral mode (gadget) device
80    private UsbMidiDevice mPeripheralMidiDevice = null;
81
82    private final class AlsaDevice {
83        public static final int TYPE_UNKNOWN = 0;
84        public static final int TYPE_PLAYBACK = 1;
85        public static final int TYPE_CAPTURE = 2;
86        public static final int TYPE_MIDI = 3;
87
88        public int mCard;
89        public int mDevice;
90        public int mType;
91
92        public AlsaDevice(int type, int card, int device) {
93            mType = type;
94            mCard = card;
95            mDevice = device;
96        }
97
98        public boolean equals(Object obj) {
99            if (! (obj instanceof AlsaDevice)) {
100                return false;
101            }
102            AlsaDevice other = (AlsaDevice)obj;
103            return (mType == other.mType && mCard == other.mCard && mDevice == other.mDevice);
104        }
105
106        public String toString() {
107            StringBuilder sb = new StringBuilder();
108            sb.append("AlsaDevice: [card: " + mCard);
109            sb.append(", device: " + mDevice);
110            sb.append(", type: " + mType);
111            sb.append("]");
112            return sb.toString();
113        }
114    }
115
116    private final FileObserver mAlsaObserver = new FileObserver(ALSA_DIRECTORY,
117            FileObserver.CREATE | FileObserver.DELETE) {
118        public void onEvent(int event, String path) {
119            switch (event) {
120                case FileObserver.CREATE:
121                    alsaFileAdded(path);
122                    break;
123                case FileObserver.DELETE:
124                    alsaFileRemoved(path);
125                    break;
126            }
127        }
128    };
129
130    /* package */ UsbAlsaManager(Context context) {
131        mContext = context;
132        mHasMidiFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);
133
134        // initial scan
135        mCardsParser.scan();
136    }
137
138    public void systemReady() {
139        mAudioService = IAudioService.Stub.asInterface(
140                        ServiceManager.getService(Context.AUDIO_SERVICE));
141
142        mAlsaObserver.startWatching();
143
144        // add existing alsa devices
145        File[] files = new File(ALSA_DIRECTORY).listFiles();
146        if (files != null) {
147            for (int i = 0; i < files.length; i++) {
148                alsaFileAdded(files[i].getName());
149            }
150        }
151    }
152
153    // Notifies AudioService when a device is added or removed
154    // audioDevice - the AudioDevice that was added or removed
155    // enabled - if true, we're connecting a device (it's arrived), else disconnecting
156    private void notifyDeviceState(UsbAudioDevice audioDevice, boolean enabled) {
157        if (DEBUG) {
158            Slog.d(TAG, "notifyDeviceState " + enabled + " " + audioDevice);
159        }
160
161        if (mAudioService == null) {
162            Slog.e(TAG, "no AudioService");
163            return;
164        }
165
166        // FIXME Does not yet handle the case where the setting is changed
167        // after device connection.  Ideally we should handle the settings change
168        // in SettingsObserver. Here we should log that a USB device is connected
169        // and disconnected with its address (card , device) and force the
170        // connection or disconnection when the setting changes.
171        int isDisabled = Settings.Secure.getInt(mContext.getContentResolver(),
172                Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0);
173        if (isDisabled != 0) {
174            return;
175        }
176
177        int state = (enabled ? 1 : 0);
178        int alsaCard = audioDevice.mCard;
179        int alsaDevice = audioDevice.mDevice;
180        if (alsaCard < 0 || alsaDevice < 0) {
181            Slog.e(TAG, "Invalid alsa card or device alsaCard: " + alsaCard +
182                        " alsaDevice: " + alsaDevice);
183            return;
184        }
185
186        String address = AudioService.makeAlsaAddressString(alsaCard, alsaDevice);
187        try {
188            // Playback Device
189            if (audioDevice.mHasPlayback) {
190                int device = (audioDevice == mAccessoryAudioDevice ?
191                        AudioSystem.DEVICE_OUT_USB_ACCESSORY :
192                        AudioSystem.DEVICE_OUT_USB_DEVICE);
193                if (DEBUG) {
194                    Slog.i(TAG, "pre-call device:0x" + Integer.toHexString(device) +
195                            " addr:" + address + " name:" + audioDevice.mDeviceName);
196                }
197                mAudioService.setWiredDeviceConnectionState(
198                        device, state, address, audioDevice.mDeviceName, TAG);
199            }
200
201            // Capture Device
202            if (audioDevice.mHasCapture) {
203               int device = (audioDevice == mAccessoryAudioDevice ?
204                        AudioSystem.DEVICE_IN_USB_ACCESSORY :
205                        AudioSystem.DEVICE_IN_USB_DEVICE);
206                mAudioService.setWiredDeviceConnectionState(
207                        device, state, address, audioDevice.mDeviceName, TAG);
208            }
209        } catch (RemoteException e) {
210            Slog.e(TAG, "RemoteException in setWiredDeviceConnectionState");
211        }
212    }
213
214    private AlsaDevice waitForAlsaDevice(int card, int device, int type) {
215        if (DEBUG) {
216            Slog.e(TAG, "waitForAlsaDevice(c:" + card + " d:" + device + ")");
217        }
218
219        AlsaDevice testDevice = new AlsaDevice(type, card, device);
220
221        // This value was empirically determined.
222        final int kWaitTime = 2500; // ms
223
224        synchronized(mAlsaDevices) {
225            long timeout = SystemClock.elapsedRealtime() + kWaitTime;
226            do {
227                if (mAlsaDevices.values().contains(testDevice)) {
228                    return testDevice;
229                }
230                long waitTime = timeout - SystemClock.elapsedRealtime();
231                if (waitTime > 0) {
232                    try {
233                        mAlsaDevices.wait(waitTime);
234                    } catch (InterruptedException e) {
235                        Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file.");
236                    }
237                }
238            } while (timeout > SystemClock.elapsedRealtime());
239        }
240
241        Slog.e(TAG, "waitForAlsaDevice failed for " + testDevice);
242        return null;
243    }
244
245    private void alsaFileAdded(String name) {
246        int type = AlsaDevice.TYPE_UNKNOWN;
247        int card = -1, device = -1;
248
249        if (name.startsWith("pcmC")) {
250            if (name.endsWith("p")) {
251                type = AlsaDevice.TYPE_PLAYBACK;
252            } else if (name.endsWith("c")) {
253                type = AlsaDevice.TYPE_CAPTURE;
254            }
255        } else if (name.startsWith("midiC")) {
256            type = AlsaDevice.TYPE_MIDI;
257        }
258
259        if (type != AlsaDevice.TYPE_UNKNOWN) {
260            try {
261                int c_index = name.indexOf('C');
262                int d_index = name.indexOf('D');
263                int end = name.length();
264                if (type == AlsaDevice.TYPE_PLAYBACK || type == AlsaDevice.TYPE_CAPTURE) {
265                    // skip trailing 'p' or 'c'
266                    end--;
267                }
268                card = Integer.parseInt(name.substring(c_index + 1, d_index));
269                device = Integer.parseInt(name.substring(d_index + 1, end));
270            } catch (Exception e) {
271                Slog.e(TAG, "Could not parse ALSA file name " + name, e);
272                return;
273            }
274            synchronized(mAlsaDevices) {
275                if (mAlsaDevices.get(name) == null) {
276                    AlsaDevice alsaDevice = new AlsaDevice(type, card, device);
277                    Slog.d(TAG, "Adding ALSA device " + alsaDevice);
278                    mAlsaDevices.put(name, alsaDevice);
279                    mAlsaDevices.notifyAll();
280                }
281            }
282        }
283    }
284
285    private void alsaFileRemoved(String path) {
286        synchronized(mAlsaDevices) {
287            AlsaDevice device = mAlsaDevices.remove(path);
288            if (device != null) {
289                Slog.d(TAG, "ALSA device removed: " + device);
290            }
291        }
292    }
293
294    /*
295     * Select the default device of the specified card.
296     */
297    /* package */ UsbAudioDevice selectAudioCard(int card) {
298        if (DEBUG) {
299            Slog.d(TAG, "selectAudioCard() card:" + card
300                    + " isCardUsb(): " + mCardsParser.isCardUsb(card));
301        }
302        if (!mCardsParser.isCardUsb(card)) {
303            // Don't. AudioPolicyManager has logic for falling back to internal devices.
304            return null;
305        }
306
307        mDevicesParser.scan();
308        int device = mDevicesParser.getDefaultDeviceNum(card);
309
310        boolean hasPlayback = mDevicesParser.hasPlaybackDevices(card);
311        boolean hasCapture = mDevicesParser.hasCaptureDevices(card);
312        if (DEBUG) {
313            Slog.d(TAG, "usb: hasPlayback:" + hasPlayback + " hasCapture:" + hasCapture);
314        }
315
316        int deviceClass =
317            (mCardsParser.isCardUsb(card)
318                ? UsbAudioDevice.kAudioDeviceClass_External
319                : UsbAudioDevice.kAudioDeviceClass_Internal) |
320            UsbAudioDevice.kAudioDeviceMeta_Alsa;
321
322        // Playback device file needed/present?
323        if (hasPlayback && (waitForAlsaDevice(card, device, AlsaDevice.TYPE_PLAYBACK) == null)) {
324            return null;
325        }
326
327        // Capture device file needed/present?
328        if (hasCapture && (waitForAlsaDevice(card, device, AlsaDevice.TYPE_CAPTURE) == null)) {
329            return null;
330        }
331
332        UsbAudioDevice audioDevice =
333                new UsbAudioDevice(card, device, hasPlayback, hasCapture, deviceClass);
334        AlsaCardsParser.AlsaCardRecord cardRecord = mCardsParser.getCardRecordFor(card);
335        audioDevice.mDeviceName = cardRecord.mCardName;
336        audioDevice.mDeviceDescription = cardRecord.mCardDescription;
337
338        notifyDeviceState(audioDevice, true);
339
340        return audioDevice;
341    }
342
343    /* package */ UsbAudioDevice selectDefaultDevice() {
344        if (DEBUG) {
345            Slog.d(TAG, "UsbAudioManager.selectDefaultDevice()");
346        }
347        return selectAudioCard(mCardsParser.getDefaultCard());
348    }
349
350    /* package */ void usbDeviceAdded(UsbDevice usbDevice) {
351       if (DEBUG) {
352          Slog.d(TAG, "deviceAdded(): " + usbDevice.getManufacturerName() +
353                  " nm:" + usbDevice.getProductName());
354        }
355
356        // Is there an audio interface in there?
357        boolean isAudioDevice = false;
358
359        // FIXME - handle multiple configurations?
360        int interfaceCount = usbDevice.getInterfaceCount();
361        for (int ntrfaceIndex = 0; !isAudioDevice && ntrfaceIndex < interfaceCount;
362                ntrfaceIndex++) {
363            UsbInterface ntrface = usbDevice.getInterface(ntrfaceIndex);
364            if (ntrface.getInterfaceClass() == UsbConstants.USB_CLASS_AUDIO) {
365                isAudioDevice = true;
366            }
367        }
368
369        if (DEBUG) {
370            Slog.d(TAG, "  isAudioDevice: " + isAudioDevice);
371        }
372        if (!isAudioDevice) {
373            return;
374        }
375
376        int addedCard = mCardsParser.getDefaultUsbCard();
377
378        // If the default isn't a USB device, let the existing "select internal mechanism"
379        // handle the selection.
380        if (DEBUG) {
381            Slog.d(TAG, "  mCardsParser.isCardUsb(" + addedCard + ") = "
382                        + mCardsParser.isCardUsb(addedCard));
383        }
384        if (mCardsParser.isCardUsb(addedCard)) {
385            UsbAudioDevice audioDevice = selectAudioCard(addedCard);
386            if (audioDevice != null) {
387                mAudioDevices.put(usbDevice, audioDevice);
388                Slog.i(TAG, "USB Audio Device Added: " + audioDevice);
389            }
390
391            // look for MIDI devices
392
393            // Don't need to call mDevicesParser.scan() because selectAudioCard() does this above.
394            // Uncomment this next line if that behavior changes in the fugure.
395            // mDevicesParser.scan()
396
397            boolean hasMidi = mDevicesParser.hasMIDIDevices(addedCard);
398            if (hasMidi && mHasMidiFeature) {
399                int device = mDevicesParser.getDefaultDeviceNum(addedCard);
400                AlsaDevice alsaDevice = waitForAlsaDevice(addedCard, device, AlsaDevice.TYPE_MIDI);
401                if (alsaDevice != null) {
402                    Bundle properties = new Bundle();
403                    String manufacturer = usbDevice.getManufacturerName();
404                    String product = usbDevice.getProductName();
405                    String version = usbDevice.getVersion();
406                    String name;
407                    if (manufacturer == null || manufacturer.isEmpty()) {
408                        name = product;
409                    } else if (product == null || product.isEmpty()) {
410                        name = manufacturer;
411                    } else {
412                        name = manufacturer + " " + product;
413                    }
414                    properties.putString(MidiDeviceInfo.PROPERTY_NAME, name);
415                    properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer);
416                    properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product);
417                    properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version);
418                    properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
419                            usbDevice.getSerialNumber());
420                    properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, alsaDevice.mCard);
421                    properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, alsaDevice.mDevice);
422                    properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
423
424                    UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
425                            alsaDevice.mCard, alsaDevice.mDevice);
426                    if (usbMidiDevice != null) {
427                        mMidiDevices.put(usbDevice, usbMidiDevice);
428                    }
429                }
430            }
431        }
432
433        if (DEBUG) {
434            Slog.d(TAG, "deviceAdded() - done");
435        }
436    }
437
438    /* package */ void usbDeviceRemoved(UsbDevice usbDevice) {
439        if (DEBUG) {
440          Slog.d(TAG, "deviceRemoved(): " + usbDevice.getManufacturerName() +
441                  " " + usbDevice.getProductName());
442        }
443
444        UsbAudioDevice audioDevice = mAudioDevices.remove(usbDevice);
445        Slog.i(TAG, "USB Audio Device Removed: " + audioDevice);
446        if (audioDevice != null) {
447            if (audioDevice.mHasPlayback || audioDevice.mHasCapture) {
448                notifyDeviceState(audioDevice, false);
449
450                // if there any external devices left, select one of them
451                selectDefaultDevice();
452            }
453        }
454        UsbMidiDevice usbMidiDevice = mMidiDevices.remove(usbDevice);
455        if (usbMidiDevice != null) {
456            IoUtils.closeQuietly(usbMidiDevice);
457        }
458    }
459
460   /* package */ void setAccessoryAudioState(boolean enabled, int card, int device) {
461       if (DEBUG) {
462            Slog.d(TAG, "setAccessoryAudioState " + enabled + " " + card + " " + device);
463        }
464        if (enabled) {
465            mAccessoryAudioDevice = new UsbAudioDevice(card, device, true, false,
466                    UsbAudioDevice.kAudioDeviceClass_External);
467            notifyDeviceState(mAccessoryAudioDevice, true);
468        } else if (mAccessoryAudioDevice != null) {
469            notifyDeviceState(mAccessoryAudioDevice, false);
470            mAccessoryAudioDevice = null;
471        }
472    }
473
474   /* package */ void setPeripheralMidiState(boolean enabled, int card, int device) {
475        if (!mHasMidiFeature) {
476            return;
477        }
478
479        if (enabled && mPeripheralMidiDevice == null) {
480            Bundle properties = new Bundle();
481            Resources r = mContext.getResources();
482            properties.putString(MidiDeviceInfo.PROPERTY_NAME, r.getString(
483                    com.android.internal.R.string.usb_midi_peripheral_name));
484            properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, r.getString(
485                    com.android.internal.R.string.usb_midi_peripheral_manufacturer_name));
486            properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, r.getString(
487                    com.android.internal.R.string.usb_midi_peripheral_product_name));
488            properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card);
489            properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device);
490            mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device);
491        } else if (!enabled && mPeripheralMidiDevice != null) {
492            IoUtils.closeQuietly(mPeripheralMidiDevice);
493            mPeripheralMidiDevice = null;
494        }
495   }
496
497    //
498    // Devices List
499    //
500    public ArrayList<UsbAudioDevice> getConnectedDevices() {
501        ArrayList<UsbAudioDevice> devices = new ArrayList<UsbAudioDevice>(mAudioDevices.size());
502        for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) {
503            devices.add(entry.getValue());
504        }
505        return devices;
506    }
507
508    //
509    // Logging
510    //
511    public void dump(IndentingPrintWriter pw) {
512        pw.println("USB Audio Devices:");
513        for (UsbDevice device : mAudioDevices.keySet()) {
514            pw.println("  " + device.getDeviceName() + ": " + mAudioDevices.get(device));
515        }
516        pw.println("USB MIDI Devices:");
517        for (UsbDevice device : mMidiDevices.keySet()) {
518            pw.println("  " + device.getDeviceName() + ": " + mMidiDevices.get(device));
519        }
520    }
521
522    public void logDevicesList(String title) {
523      if (DEBUG) {
524          for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) {
525              Slog.i(TAG, "UsbDevice-------------------");
526              Slog.i(TAG, "" + (entry != null ? entry.getKey() : "[none]"));
527              Slog.i(TAG, "UsbAudioDevice--------------");
528              Slog.i(TAG, "" + entry.getValue());
529          }
530      }
531  }
532
533  // This logs a more terse (and more readable) version of the devices list
534  public void logDevices(String title) {
535      if (DEBUG) {
536          Slog.i(TAG, title);
537          for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) {
538              Slog.i(TAG, entry.getValue().toShortString());
539          }
540      }
541  }
542}
543