Remove polling for ALSA device files in USB audio device detection.

Change-Id: I993fe41707fc02c9449800c7325aaf7270fb892e
diff --git a/services/usb/java/com/android/server/usb/UsbAudioManager.java b/services/usb/java/com/android/server/usb/UsbAudioManager.java
index bb45ee8..95eb92b 100644
--- a/services/usb/java/com/android/server/usb/UsbAudioManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAudioManager.java
@@ -24,6 +24,8 @@
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbInterface;
 import android.media.AudioManager;
+import android.os.FileObserver;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.Slog;
 
@@ -39,6 +41,8 @@
     private static final String TAG = UsbAudioManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
+    private static final String ALSA_DIRECTORY = "/dev/snd/";
+
     private final Context mContext;
 
     private final class AudioDevice {
@@ -69,13 +73,74 @@
         }
     }
 
+    private final class AlsaDevice {
+        public static final int TYPE_UNKNOWN = 0;
+        public static final int TYPE_PLAYBACK = 1;
+        public static final int TYPE_CAPTURE = 2;
+        public static final int TYPE_MIDI = 3;
+
+        public int mCard;
+        public int mDevice;
+        public int mType;
+
+        public AlsaDevice(int type, int card, int device) {
+            mType = type;
+            mCard = card;
+            mDevice = device;
+        }
+
+        public boolean equals(Object obj) {
+            if (! (obj instanceof AlsaDevice)) {
+                return false;
+            }
+            AlsaDevice other = (AlsaDevice)obj;
+            return (mType == other.mType && mCard == other.mCard && mDevice == other.mDevice);
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("AlsaDevice: [card: " + mCard);
+            sb.append(", device: " + mDevice);
+            sb.append(", type: " + mType);
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
     private final HashMap<UsbDevice,AudioDevice> mAudioDevices
             = new HashMap<UsbDevice,AudioDevice>();
 
+    private final HashMap<String,AlsaDevice> mAlsaDevices
+            = new HashMap<String,AlsaDevice>();
+
+    private final FileObserver mAlsaObserver = new FileObserver(ALSA_DIRECTORY,
+            FileObserver.CREATE | FileObserver.DELETE) {
+        public void onEvent(int event, String path) {
+            switch (event) {
+                case FileObserver.CREATE:
+                    alsaFileAdded(path);
+                    break;
+                case FileObserver.DELETE:
+                    alsaFileRemoved(path);
+                    break;
+            }
+        }
+    };
+
     /* package */ UsbAudioManager(Context context) {
         mContext = context;
     }
 
+    public void systemReady() {
+        mAlsaObserver.startWatching();
+
+        // add existing alsa devices
+        File[] files = new File(ALSA_DIRECTORY).listFiles();
+        for (int i = 0; i < files.length; i++) {
+            alsaFileAdded(files[i].getName());
+        }
+    }
+
     // Broadcasts the arrival/departure of a USB audio interface
     // audioDevice - the AudioDevice that was added or removed
     // enabled - if true, we're connecting a device (it's arrived), else disconnecting
@@ -93,30 +158,87 @@
         mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
     }
 
-    private boolean waitForAlsaFile(int card, int device, boolean capture) {
-        // These values were empirically determined.
-        final int kNumRetries = 5;
-        final int kSleepTime = 500; // ms
-        String alsaDevPath = "/dev/snd/pcmC" + card + "D" + device + (capture ? "c" : "p");
-        File alsaDevFile = new File(alsaDevPath);
-        boolean exists = false;
-        for (int retry = 0; !exists && retry < kNumRetries; retry++) {
-            exists = alsaDevFile.exists();
-            if (!exists) {
-                try {
-                    Thread.sleep(kSleepTime);
-                } catch (IllegalThreadStateException ex) {
-                    Slog.d(TAG, "usb: IllegalThreadStateException while waiting for ALSA file.");
-                } catch (java.lang.InterruptedException ex) {
-                    Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file.");
+    private AlsaDevice waitForAlsaDevice(int card, int device, int type) {
+        AlsaDevice testDevice = new AlsaDevice(type, card, device);
+
+        // This value was empirically determined.
+        final int kWaitTime = 2500; // ms
+
+        synchronized(mAlsaDevices) {
+            long timeout = SystemClock.elapsedRealtime() + kWaitTime;
+            do {
+                if (mAlsaDevices.values().contains(testDevice)) {
+                    return testDevice;
+                }
+                long waitTime = timeout - SystemClock.elapsedRealtime();
+                if (waitTime > 0) {
+                    try {
+                        mAlsaDevices.wait(waitTime);
+                    } catch (InterruptedException e) {
+                        Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file.");
+                    }
+                }
+            } while (timeout > SystemClock.elapsedRealtime());
+        }
+
+        Slog.e(TAG, "waitForAlsaDevice failed for " + testDevice);
+        return null;
+    }
+
+    private void alsaFileAdded(String name) {
+        int type = AlsaDevice.TYPE_UNKNOWN;
+        int card = -1, device = -1;
+
+        if (name.startsWith("pcmC")) {
+            if (name.endsWith("p")) {
+                type = AlsaDevice.TYPE_PLAYBACK;
+            } else if (name.endsWith("c")) {
+                type = AlsaDevice.TYPE_CAPTURE;
+            }
+        } else if (name.startsWith("midiC")) {
+            type = AlsaDevice.TYPE_MIDI;
+        }
+
+        if (type != AlsaDevice.TYPE_UNKNOWN) {
+            try {
+                int c_index = name.indexOf('C');
+                int d_index = name.indexOf('D');
+                int end = name.length();
+                if (type == AlsaDevice.TYPE_PLAYBACK || type == AlsaDevice.TYPE_CAPTURE) {
+                    // skip trailing 'p' or 'c'
+                    end--;
+                }
+                card = Integer.parseInt(name.substring(c_index + 1, d_index));
+                device = Integer.parseInt(name.substring(d_index + 1, end));
+            } catch (Exception e) {
+                Slog.e(TAG, "Could not parse ALSA file name " + name, e);
+                return;
+            }
+            synchronized(mAlsaDevices) {
+                if (mAlsaDevices.get(name) == null) {
+                    AlsaDevice alsaDevice = new AlsaDevice(type, card, device);
+                    Slog.d(TAG, "Adding ALSA device " + alsaDevice);
+                    mAlsaDevices.put(name, alsaDevice);
+                    mAlsaDevices.notifyAll();
                 }
             }
         }
+    }
 
-        return exists;
+    private void alsaFileRemoved(String path) {
+        synchronized(mAlsaDevices) {
+            AlsaDevice device = mAlsaDevices.remove(path);
+            if (device != null) {
+                Slog.d(TAG, "ALSA device removed: " + device);
+            }
+        }
     }
 
     /* package */ void deviceAdded(UsbDevice usbDevice) {
+       if (DEBUG) {
+          Slog.d(TAG, "deviceAdded(): " + usbDevice);
+        }
+
         // Is there an audio interface in there?
         boolean isAudioDevice = false;
 
@@ -134,7 +256,7 @@
         }
 
         //TODO(pmclean) The "Parser" objects inspect files in "/proc/asound" which we presume is
-        // present, unlike the waitForAlsaFile() which waits on a file in /dev/snd. It is not
+        // present, unlike the waitForAlsaDevice() which waits on a file in /dev/snd. It is not
         // clear why this works, or that it can be relied on going forward.  Needs further
         // research.
         AlsaCardsParser cardsParser = new AlsaCardsParser();
@@ -157,19 +279,21 @@
 
         // Playback device file needed/present?
         if (hasPlayback &&
-            !waitForAlsaFile(card, device, false)) {
+            waitForAlsaDevice(card, device, AlsaDevice.TYPE_PLAYBACK) == null) {
             return;
         }
 
         // Capture device file needed/present?
         if (hasCapture &&
-            !waitForAlsaFile(card, device, true)) {
+            waitForAlsaDevice(card, device, AlsaDevice.TYPE_CAPTURE) == null) {
             return;
         }
 
         if (DEBUG) {
             Slog.d(TAG,
-                    "usb: hasPlayback:" + hasPlayback + " hasCapture:" + hasCapture);
+                    "usb: hasPlayback:" + hasPlayback +
+                    " hasCapture:" + hasCapture +
+                    " hasMidi:" + hasMidi);
         }
 
         AudioDevice audioDevice = new AudioDevice(card, device, hasPlayback, hasCapture, hasMidi);
@@ -184,7 +308,9 @@
 
         AudioDevice audioDevice = mAudioDevices.remove(device);
         if (audioDevice != null) {
-            sendDeviceNotification(audioDevice, false);
+            if (audioDevice.mHasPlayback || audioDevice.mHasPlayback) {
+                sendDeviceNotification(audioDevice, false);
+            }
         }
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index e769bda..2fc8fd3 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -245,6 +245,8 @@
     }
 
     public void systemReady() {
+        mUsbAudioManager.systemReady();
+
         synchronized (mLock) {
             // Create a thread to call into native code to wait for USB host events.
             // This thread will call us back on usbDeviceAdded and usbDeviceRemoved.