jabra-vold: Fix volume unsync issue after hotplug

This change is to fix the volume unsync issue as detailed in
the bug. The underlying cause is that usbmon interface can no
longer receive packets from Jabra device after hotplug.
With lots of testing we concluded two things needs to be fixed
in order to make Jabra devices work well across hotplug.

1. After jabra-vold is launched, an extra volume read-then-write
  is required so that usbmon can continue to receive packets.
2. During the init stage, each jabra daemon should NOT add all
  volume controls to the list for later volume setting. That could
  cause trouble because a control is still being referenced even
  the physical Jabra is unplugged. To fix this we need to look up
  sound card index at initialize stage and use it to filter volume
  controls on the system.

BUG=chromium:653679
TEST=Test on Panther Hotrod mode with two Jabra speakerphones,
hotplug Jabra releatedly to verify that volume sync can work.

Change-Id: Id31e3b5628d71eb8bdc9546124376eb0e64f8042
Reviewed-on: https://chromium-review.googlesource.com/398898
Commit-Ready: Hsinyu Chao <hychao@chromium.org>
Tested-by: Hsinyu Chao <hychao@chromium.org>
Reviewed-by: Chinyue Chen <chinyue@chromium.org>
diff --git a/99-jabra.rules b/99-jabra.rules
index 151bc16..cffda45 100644
--- a/99-jabra.rules
+++ b/99-jabra.rules
@@ -17,7 +17,7 @@
 # start the jabra_vold daemon
 ACTION=="add", RUN+="/bin/mkdir -p /var/run/jabra_vold"
 ACTION=="add", RUN+="/bin/chown volume:volume /var/run/jabra_vold"
-ACTION=="add", RUN+="/sbin/minijail0 -u volume -g volume -G -- /usr/sbin/jabra_vold -a start -n $env{DEVNUM} -b $env{BUSNUM}"
+ACTION=="add", RUN+="/sbin/minijail0 -u volume -g volume -G -- /usr/sbin/jabra_vold -a start -n $env{DEVNUM} -b $env{BUSNUM} -p /sys/$env{DEVPATH}"
 # stop the daemon
 ACTION=="remove", RUN+="/sbin/minijail0 -u volume -g volume -G -- /usr/sbin/jabra_vold -a stop -n $env{DEVNUM} -b $env{BUSNUM}"
 
diff --git a/jabra.c b/jabra.c
index 8c78db9..aa5c2c7 100644
--- a/jabra.c
+++ b/jabra.c
@@ -15,6 +15,7 @@
 #include <syslog.h>
 #include <sys/types.h>
 #include <sys/wait.h>
+#include <dirent.h>
 #include <time.h>
 
 #include <alsa/asoundlib.h>
@@ -43,6 +44,7 @@
 /* Names used by the kernel Alsa audio driver */
 #define ALSA_DEV_NAME "Jabra SPEAK"
 #define ALSA_CONTROL_NAME "PCM Playback Volume"
+#define CARD_PREFIX "card"
 
 #define KEY_CROS_VOLDOWN KEY_VOLUMEDOWN
 #define KEY_CROS_VOLUP KEY_VOLUMEUP
@@ -91,6 +93,7 @@
 	snd_hctl_elem_t *playback_vol[ALSA_DEV_COUNT_MAX];
 	int mon_fd;
 	int devnum;
+	int alsa_card_idx;
 };
 
 static void logd(int level, const char *fmt, ...)
@@ -140,6 +143,8 @@
 	while (time(NULL) < deadline) {
 		/* find the Jabra device id */
 		for (idx = -1; (snd_card_next(&idx) == 0) && (idx >= 0); ) {
+			if ((-1 != dev->alsa_card_idx) && (idx != dev->alsa_card_idx))
+				continue;
 			ret = snd_card_get_name(idx, &card_name);
 			if (!ret && (strncmp(card_name, ALSA_DEV_NAME,
 				sizeof(ALSA_DEV_NAME) - 1) == 0)) {
@@ -376,7 +381,65 @@
 	return ret;
 }
 
-static int use_jabra_device(int busnum, int devnum, int loglvl, int errlog)
+/* Search for the sound card index under /sys/devices/, the directory
+ * path starts with the DEVPATH environment variable provided by udev,
+ * and follow by the subdirectory to sound subsustem. Example:
+ * /sys/devices/pci0000:00/0000:00:14.0/usb1/1-6/1-6:1.0/sound/
+ */
+static int find_card(char *devpath, int busnum, int devnum)
+{
+	char path[256];
+	char *tokens, *tmp, *last;
+	DIR * dir;
+	struct dirent * ptr;
+	int alsa_card_idx = -1;
+	int opendir_retry = 5;
+
+	/* Extract the last layer of directory from devpath. i.e
+	 * '1-6' from /sys/devices/pci0000:00/0000:00:14.0/usb1/1-6
+	 */
+	last = NULL;
+	tokens = strdup(devpath);
+	tmp = strtok(tokens, "/");
+	while (tmp) {
+		last = tmp;
+		tmp = strtok(NULL, "/");
+	}
+	snprintf(path, sizeof(path), "%s/%s:1.0/sound/", devpath, last);
+	free(tokens);
+
+usb_open_dir:
+	dir = opendir(path);
+	if (dir == NULL) {
+		if (errno == ENOENT) {
+			if (opendir_retry-- == 0) {
+				logd(LOG_ERR, "Open %s timeout\n", path);
+				return -EINVAL;
+			}
+			usleep(100000);
+			goto usb_open_dir;
+		}
+		logd(LOG_ERR, "Open %s fail, %s\n", path, strerror(errno));
+		return -errno;
+	}
+
+	/* Search for the file named 'cardX' where X is the sound card index
+	 * given by ALSA framework. */
+	while((ptr = readdir(dir)) != NULL) {
+		if (strncmp(CARD_PREFIX, ptr->d_name, sizeof(CARD_PREFIX) - 1))
+			continue;
+
+		alsa_card_idx = atoi(ptr->d_name + sizeof(CARD_PREFIX) - 1);
+		logd(LOG_INFO, "Successfully found card %d by parsing %s\n",
+		     alsa_card_idx, path);
+		break;
+	}
+
+	closedir(dir);
+	return alsa_card_idx;
+}
+
+static int use_jabra_device(char *devpath, int busnum, int devnum, int loglvl, int errlog)
 {
 	struct jabra_dev *dev;
 	int idx, ret;
@@ -394,6 +457,14 @@
 	if (ret)
 		goto err;
 
+	/* Look up the sound card index before alsa_init(). Set to -1 if
+	 * card index cannot be found, in which case alsa_init() will
+	 * keep all volume controls from all sound cards to use. */
+	if (devpath) {
+		ret = find_card(devpath, busnum, devnum);
+		dev->alsa_card_idx = (ret < 0) ? -1 : ret;
+	}
+
 	ret = alsa_init(dev);
 	if (ret)
 		goto err;
@@ -402,6 +473,9 @@
 	if (ret)
 		goto err;
 
+	/* Set volume is required to receive HID packets afterwards. */
+	alsa_touch_volume(dev);
+
 	usbmon_poll(dev);
 err:
 	if (dev->mon_fd)
@@ -426,13 +500,14 @@
 	char *busnum_str = getenv("BUSNUM");
 	char *devnum_str = getenv("DEVNUM");
 	char *loglevel_str = getenv("LOGLEVEL");
+	char *devpath = getenv("DEVPATH");
 	char pid_filename[128];
 	int use_stderr = 0;
 	int busnum, devnum;
 	int loglevel = LOG_NOTICE;
 	int pid;
 
-	while ((opt = getopt(argc, argv, "a:b:d:n:")) != -1) {
+	while ((opt = getopt(argc, argv, "a:b:d:n:p:")) != -1) {
 		switch (opt) {
 		case 'a':
 			action_str = optarg;
@@ -447,6 +522,10 @@
 		case 'n':
 			devnum_str = optarg;
 			break;
+		case 'p':
+			if (optarg)
+				devpath = optarg;
+			break;
 		}
 	}
 
@@ -503,12 +582,12 @@
 	} else if (strcmp(action_str, "stop") == 0) {
 		FILE* pid_file = fopen(pid_filename, "r");
 		if (!pid_file) {
-			fprintf(stderr, "Failed to open PID file to read for jabra_vold %d:%d\n",
+			logd(LOG_ERR, "Failed to open PID file to read for jabra_vold %d:%d\n",
 					busnum, devnum);
 			return -ENOMEM;
 		}
 		if (fscanf(pid_file, "%d", &pid) <= 0) {
-			fprintf(stderr, "Failed to obtain PID for jabra_vold %d:%d\n",
+			logd(LOG_ERR, "Failed to obtain PID for jabra_vold %d:%d\n",
 					busnum, devnum);
 			return -ENOMEM;
 		}
@@ -522,5 +601,5 @@
 		return -EINVAL;
 	}
 
-	return use_jabra_device(busnum, devnum, loglevel, use_stderr);
+	return use_jabra_device(devpath, busnum, devnum, loglevel, use_stderr);
 }