[autotest] audio_extension_handler: Use new chrome.audio API

Use new API for the changes in crbug.com/673392.

- Replace getInfo with getDevices
- Replace setProperties with setMute to mute system.

BUG=chromium:699170, chromium:699007
TEST=test_that --fast --debug --board=auron_paine
  --args="chameleon_host=chromeos2-row10-rack9-host9-chameleon.cros"
  chromeos2-row10-rack9-host9.cros audio_AudioBasicHDMI
TEST=check multimedia_xmlrpc_server audio facade.
  >>> s.browser.start_default_chrome()
  True
  >>> s.audio.set_chrome_active_node_type('INTERNAL_SPEAKER', 'INTERNAL_MIC')
  >>> s.audio.set_chrome_active_node_type('HEADPHONE', 'MIC')
  >>> s.audio.get_audio_devices()
  [{'deviceName': 'Post Mix Pre DSP Loopback', 'displayName': 'Post Mix
  Pre DSP Loopback', 'level': 75, 'stableDeviceId': '2315562897',
  'streamType': 'INPUT', 'deviceType': 'POST_MIX_LOOPBACK', 'id':
  '12884901888', 'isActive': False}, {'deviceName': 'Post DSP Loopback',
  'displayName': 'Post DSP Loopback', 'level': 75, 'stableDeviceId':
  '2356475750', 'streamType': 'INPUT', 'deviceType': 'POST_DSP_LOOPBACK',
  'id': '17179869184', 'isActive': False}, {'deviceName': 'sklnau8825adi:
  :0,0', 'displayName': 'Headphone', 'level': 75, 'stableDeviceId':
  '2660450915', 'streamType': 'OUTPUT', 'deviceType': 'HEADPHONE', 'id':
  '21474836480', 'isActive': True}, {'deviceName': 'sklnau8825adi: :0,0',
  'displayName': 'Speaker', 'level': 75, 'stableDeviceId': '1923447123',
  'streamType': 'OUTPUT', 'deviceType': 'INTERNAL_SPEAKER', 'id':
  '21474836481', 'isActive': False}, {'deviceName': 'sklnau8825adi: :0,1',
  'displayName': 'Mic', 'level': 75, 'stableDeviceId': '1638577850',
  'streamType': 'INPUT', 'deviceType': 'MIC', 'id': '25769803776',
  'isActive': True}, {'deviceName': 'sklnau8825adi: :0,1', 'displayName':
  'Internal Mic', 'level': 75, 'stableDeviceId': '1289939621',
  'streamType': 'INPUT', 'deviceType': 'INTERNAL_MIC', 'id':
  '25769803777', 'isActive': False}]
  >>> s.audio.get_chrome_active_volume_mute()
  [75, False]
  >>> s.set_chrome_active_volume(50)
  >>> s.audio.set_chrome_active_volume(50)
  >>> s.audio.get_chrome_active_volume_mute()
  [50, False]
  >>> s.audio.set_chrome_mute(True)
  >>> s.audio.get_chrome_active_volume_mute()
  [50, True]

Change-Id: If6ea4db43df9f3ed32100f10dc7d1b1219831f2e
Reviewed-on: https://chromium-review.googlesource.com/450833
Commit-Ready: Cheng-Yi Chiang <cychiang@chromium.org>
Tested-by: Cheng-Yi Chiang <cychiang@chromium.org>
Reviewed-by: Kalin Stoyanov <kalin@chromium.org>
Reviewed-by: Hsu Wei-Cheng <mojahsu@chromium.org>
(cherry picked from commit 402ddbdc1ec1151be2f47f8eaa1c6614196a70f9)
Reviewed-on: https://chromium-review.googlesource.com/471186
Reviewed-by: Cheng-Yi Chiang <cychiang@chromium.org>
Commit-Queue: Cheng-Yi Chiang <cychiang@chromium.org>
diff --git a/client/cros/multimedia/audio_extension_handler.py b/client/cros/multimedia/audio_extension_handler.py
index 3d8d21e..feef128 100644
--- a/client/cros/multimedia/audio_extension_handler.py
+++ b/client/cros/multimedia/audio_extension_handler.py
@@ -10,10 +10,14 @@
 from autotest_lib.client.cros.multimedia import facade_resource
 
 class AudioExtensionHandlerError(Exception):
+    """Class for exceptions thrown from the AudioExtensionHandler"""
     pass
 
 
 class AudioExtensionHandler(object):
+    """Wrapper around test extension that uses chrome.audio API to get audio
+    device information
+    """
     def __init__(self, extension):
         """Initializes an AudioExtensionHandler.
 
@@ -39,34 +43,44 @@
 
 
     @facade_resource.retry_chrome_call
-    def get_audio_info(self):
-        """Gets the audio info from Chrome audio API.
+    def get_audio_devices(self, device_filter=None):
+        """Gets the audio device info from Chrome audio API.
 
-        @returns: An array of [outputInfo, inputInfo].
-                  outputInfo is an array of output node info dicts. Each dict
+        @param device_filter: Filter for returned device nodes.
+            An optional dict that can have the following properties:
+                string array streamTypes
+                    Restricts stream types that returned devices can have.
+                    It should contain "INPUT" for result to include input
+                    devices, and "OUTPUT" for results to include output devices.
+                    If not set, returned devices will not be filtered by the
+                    stream type.
+
+                boolean isActive
+                   If true, only active devices will be included in the result.
+                   If false, only inactive devices will be included in the
+                   result.
+
+            The filter param defaults to {}, requests all available audio
+            devices.
+
+        @returns: An array of audioDeviceInfo.
+                  Each audioDeviceInfo dict
                   contains these key-value pairs:
                      string  id
-                         The unique identifier of the audio output device.
+                         The unique identifier of the audio device.
 
-                     string  name
+                     string stableDeviceId
+                         The stable identifier of the audio device.
+
+                     string  streamType
+                         "INPUT" if the device is an input audio device,
+                         "OUTPUT" if the device is an output audio device.
+
+                     string displayName
                          The user-friendly name (e.g. "Bose Amplifier").
 
-                     boolean isActive
-                         True if this is the current active device.
-
-                     boolean isMuted
-                         True if this is muted.
-
-                     double  volume
-                         The output volume ranging from 0.0 to 100.0.
-
-                  inputInfo is an arrya of input node info dicts. Each dict
-                  contains these key-value pairs:
-                     string  id
-                         The unique identifier of the audio input device.
-
-                     string  name
-                         The user-friendly name (e.g. "USB Microphone").
+                     string deviceName
+                         The devuce name
 
                      boolean isActive
                          True if this is the current active device.
@@ -74,57 +88,55 @@
                      boolean isMuted
                          True if this is muted.
 
-                     double  gain
-                         The input gain ranging from 0.0 to 100.0.
+                     long level
+                         The output volume or input gain.
 
         """
-        self._extension.ExecuteJavaScript('window.__audio_info = null;')
+        def filter_to_str(device_filter):
+            """Converts python dict device filter to JS object string.
+
+            @param device_filter: Device filter dict.
+
+            @returns: Device filter as a srting representation of a
+                      JavaScript object.
+
+            """
+            return str(device_filter or {}).replace('True', 'true').replace(
+                        'False', 'false')
+
+        self._extension.ExecuteJavaScript('window.__audio_devices = null;')
         self._extension.ExecuteJavaScript(
-                "chrome.audio.getInfo(function(outputInfo, inputInfo) {"
-                "window.__audio_info = [outputInfo, inputInfo];})")
+                "chrome.audio.getDevices(%s, function(devices) {"
+                "window.__audio_devices = devices;})"
+                % filter_to_str(device_filter))
         utils.wait_for_value(
                 lambda: (self._extension.EvaluateJavaScript(
-                         "window.__audio_info") != None),
+                         "window.__audio_devices") != None),
                 expected_value=True)
-        return self._extension.EvaluateJavaScript("window.__audio_info")
+        return self._extension.EvaluateJavaScript("window.__audio_devices")
 
 
-    def _get_active_id(self):
-        """Gets active output and input node id.
-
-        Assume there is only one active output node and one active input node.
-
-        @returns: (output_id, input_id) where output_id and input_id are
-                  strings for active node id.
-
-        """
-        output_nodes, input_nodes = self.get_audio_info()
-
-        return (self._get_active_id_from_nodes(output_nodes),
-                self._get_active_id_from_nodes(input_nodes))
-
-
-    def _get_active_id_from_nodes(self, nodes):
-        """Gets active node id from nodes.
+    def _get_active_id_for_stream_type(self, stream_type):
+        """Gets active node id of the specified stream type.
 
         Assume there is only one active node.
 
-        @param nodes: A list of input/output nodes got from get_audio_info().
+        @param stream_type: 'INPUT' to get the active input device,
+                            'OUTPUT' to get the active output device.
 
-        @returns: node['id'] where node['isActive'] is True.
+        @returns: A string for the active device id.
 
         @raises: AudioExtensionHandlerError if active id is not unique.
 
         """
-        active_ids = [x['id'] for x in nodes if x['isActive']]
-        if len(active_ids) != 1:
+        nodes = self.get_audio_devices(
+            {'streamTypes': [stream_type], 'isActive': True})
+        if len(nodes) != 1:
             logging.error(
                     'Node info contains multiple active nodes: %s', nodes)
-            raise AudioExtensionHandlerError(
-                    'Active id should be unique')
+            raise AudioExtensionHandlerError('Active id should be unique')
 
-        return active_ids[0]
-
+        return nodes[0]['id']
 
 
     @facade_resource.retry_chrome_call
@@ -136,20 +148,20 @@
         @param volume: Volume to set (0~100).
 
         """
-        output_id, _ = self._get_active_id()
+        output_id = self._get_active_id_for_stream_type('OUTPUT')
         logging.debug('output_id: %s', output_id)
 
-        self._extension.ExecuteJavaScript('window.__set_volume_done = null;')
+        self.set_mute(False)
 
+        self._extension.ExecuteJavaScript('window.__set_volume_done = null;')
         self._extension.ExecuteJavaScript(
                 """
                 chrome.audio.setProperties(
                     '%s',
-                    {isMuted: false, volume: %s},
+                    {level: %s},
                     function() {window.__set_volume_done = true;});
                 """
                 % (output_id, volume))
-
         utils.wait_for_value(
                 lambda: (self._extension.EvaluateJavaScript(
                          "window.__set_volume_done") != None),
@@ -158,26 +170,22 @@
 
     @facade_resource.retry_chrome_call
     def set_mute(self, mute):
-        """Mutes the active audio output using chrome.audio API.
+        """Mutes the audio output using chrome.audio API.
 
         @param mute: True to mute. False otherwise.
 
         """
-        output_id, _ = self._get_active_id()
-        logging.debug('output_id: %s', output_id)
-
         is_muted_string = 'true' if mute else 'false'
 
         self._extension.ExecuteJavaScript('window.__set_mute_done = null;')
 
         self._extension.ExecuteJavaScript(
                 """
-                chrome.audio.setProperties(
-                    '%s',
-                    {isMuted: %s},
+                chrome.audio.setMute(
+                    'OUTPUT', %s,
                     function() {window.__set_mute_done = true;});
                 """
-                % (output_id, is_muted_string))
+                % (is_muted_string))
 
         utils.wait_for_value(
                 lambda: (self._extension.EvaluateJavaScript(
@@ -186,6 +194,24 @@
 
 
     @facade_resource.retry_chrome_call
+    def get_mute(self):
+        """Determines whether audio output is muted.
+
+        @returns Whether audio output is muted.
+
+        """
+        self._extension.ExecuteJavaScript('window.__output_muted = null;')
+        self._extension.ExecuteJavaScript(
+                "chrome.audio.getMute('OUTPUT', function(isMute) {"
+                "window.__output_muted = isMute;})")
+        utils.wait_for_value(
+                lambda: (self._extension.EvaluateJavaScript(
+                         "window.__output_muted") != None),
+                expected_value=True)
+        return self._extension.EvaluateJavaScript("window.__output_muted")
+
+
+    @facade_resource.retry_chrome_call
     def get_active_volume_mute(self):
         """Gets the volume state of active audio output using chrome.audio API.
 
@@ -193,11 +219,13 @@
                         is True if node is muted, False otherwise.
 
         """
-        output_nodes, _ = self.get_audio_info()
-        active_id = self._get_active_id_from_nodes(output_nodes)
-        for node in output_nodes:
-            if node['id'] == active_id:
-                return (node['volume'], node['isMuted'])
+        nodes = self.get_audio_devices(
+            {'streamTypes': ['OUTPUT'], 'isActive': True})
+        if len(nodes) != 1:
+            logging.error('Node info contains multiple active nodes: %s', nodes)
+            raise AudioExtensionHandlerError('Active id should be unique')
+
+        return (nodes[0]['level'], self.get_mute())
 
 
     @facade_resource.retry_chrome_call
@@ -207,14 +235,27 @@
         The current active node will be disabled first if the new active node
         is different from the current one.
 
-        @param node_id: The node id obtained from cras_utils.get_cras_nodes.
+        @param node_id: Node id obtained from cras_utils.get_cras_nodes.
                         Chrome.audio also uses this id to specify input/output
                         nodes.
+                        Note that node id returned by cras_utils.get_cras_nodes
+                        is a number, while chrome.audio API expects a string.
 
         @raises AudioExtensionHandlerError if there is no such id.
 
         """
-        if node_id in self._get_active_id():
+        nodes = self.get_audio_devices({})
+        target_node = None
+        for node in nodes:
+            if node['id'] == str(node_id):
+                target_node = node
+                break
+
+        if not target_node:
+            logging.error('Node %s not found.', node_id)
+            raise AudioExtensionHandlerError('Node id not found')
+
+        if target_node['isActive']:
             logging.debug('Node %s is already active.', node_id)
             return
 
@@ -222,13 +263,15 @@
 
         self._extension.ExecuteJavaScript('window.__set_active_done = null;')
 
+        is_input = target_node['streamType'] == 'INPUT'
+        stream_type = 'input' if is_input else 'output'
         self._extension.ExecuteJavaScript(
                 """
                 chrome.audio.setActiveDevices(
-                    ['%s'],
+                    {'%s': ['%s']},
                     function() {window.__set_active_done = true;});
                 """
-                % (node_id))
+                % (stream_type, node_id))
 
         utils.wait_for_value(
                 lambda: (self._extension.EvaluateJavaScript(
diff --git a/client/cros/multimedia/audio_facade_native.py b/client/cros/multimedia/audio_facade_native.py
index e496e6f..a894b7e 100644
--- a/client/cros/multimedia/audio_facade_native.py
+++ b/client/cros/multimedia/audio_facade_native.py
@@ -82,13 +82,13 @@
         return self._loaded_extension_handler
 
 
-    def get_audio_info(self):
-        """Returns the audio info from chrome.audio API.
+    def get_audio_devices(self):
+        """Returns the audio devices from chrome.audio API.
 
-        @returns: Checks docstring of get_audio_info of AudioExtensionHandler.
+        @returns: Checks docstring of get_audio_devices of AudioExtensionHandler.
 
         """
-        return self._extension_handler.get_audio_info()
+        return self._extension_handler.get_audio_devices()
 
 
     def set_chrome_active_volume(self, volume):