Chameleon: Separate the display-related functionality to DisplayUtility

The display-related methods are moved to DisplayUtility. We can add more
utilities later for audio and video.

BUG=chromium:407004
TEST=Ran the Chameleon test display_Resolution.mirrored and it passed.

Change-Id: Ic8c78d8a1a64760f616dd8f2492b9e05c268e7fd
Reviewed-on: https://chromium-review.googlesource.com/214423
Reviewed-by: Wai-Hong Tam <waihong@chromium.org>
Tested-by: Wai-Hong Tam <waihong@chromium.org>
Commit-Queue: Cheng-Yi Chiang <cychiang@chromium.org>
diff --git a/client/cros/multimedia/display_utility.py b/client/cros/multimedia/display_utility.py
new file mode 100644
index 0000000..3542068
--- /dev/null
+++ b/client/cros/multimedia/display_utility.py
@@ -0,0 +1,349 @@
+# Copyright 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Utility to access the display-related functionality."""
+
+import multiprocessing
+import os
+import re
+import time
+import telemetry
+
+from autotest_lib.client.bin import utils
+from autotest_lib.client.cros import constants, cros_ui, sys_power
+
+TimeoutException = telemetry.core.util.TimeoutException
+
+
+class DisplayUtility(object):
+    """Utility to access the display-related functionality."""
+
+    def __init__(self, chrome):
+        self._chrome = chrome
+        self._browser = chrome.browser
+
+
+    def get_display_info(self):
+        """Gets the display info from Chrome.system.display API.
+
+        @return array of dict for display info.
+        """
+
+        extension = self._chrome.get_extension(
+                constants.MULTIMEDIA_TEST_EXTENSION)
+        if not extension:
+            raise RuntimeError('Graphics test extension not found')
+        extension.ExecuteJavaScript('window.__display_info = null;')
+        extension.ExecuteJavaScript(
+                "chrome.system.display.getInfo(function(info) {"
+                "window.__display_info = info;})")
+        utils.wait_for_value(lambda: (
+                extension.EvaluateJavaScript("window.__display_info") != None),
+                expected_value=True)
+        return extension.EvaluateJavaScript("window.__display_info")
+
+
+    def _wait_for_display_options_to_appear(self, tab, display_index,
+                                            timeout=16):
+        """Waits for option.DisplayOptions to appear.
+
+        The function waits until options.DisplayOptions appears or is timed out
+                after the specified time.
+
+        @param tab: the tab where the display options dialog is shown.
+        @param display_index: index of the display.
+        @param timeout: time wait for display options appear.
+
+        @raise RuntimeError when display_index is out of range
+        @raise TimeoutException when the operation is timed out.
+        """
+
+        tab.WaitForJavaScriptExpression(
+                    "typeof options !== 'undefined' &&"
+                    "typeof options.DisplayOptions !== 'undefined' &&"
+                    "typeof options.DisplayOptions.instance_ !== 'undefined' &&"
+                    "typeof options.DisplayOptions.instance_"
+                    "       .displays_ !== 'undefined'", timeout)
+
+        if not tab.EvaluateJavaScript(
+                    "options.DisplayOptions.instance_.displays_.length > %d"
+                    % (display_index)):
+            raise RuntimeError('Display index out of range: '
+                    + str(tab.EvaluateJavaScript(
+                    "options.DisplayOptions.instance_.displays_.length")))
+
+        tab.WaitForJavaScriptExpression(
+                "typeof options.DisplayOptions.instance_"
+                "         .displays_[%(index)d] !== 'undefined' &&"
+                "typeof options.DisplayOptions.instance_"
+                "         .displays_[%(index)d].id !== 'undefined' &&"
+                "typeof options.DisplayOptions.instance_"
+                "         .displays_[%(index)d].resolutions !== 'undefined'"
+                % {'index': display_index}, timeout)
+
+
+    def get_display_modes(self, display_index):
+        """Gets all the display modes for the specified display.
+
+        The modes are obtained from chrome://settings-frame/display via
+        telemetry.
+
+        @param display_index: index of the display to get modes from.
+
+        @return: A list of DisplayMode dicts.
+
+        @raise TimeoutException when the operation is timed out.
+        """
+
+        tab = self._browser.tabs.New()
+        try:
+            tab.Navigate('chrome://settings-frame/display')
+            tab.Activate()
+            self._wait_for_display_options_to_appear(tab, display_index)
+            return tab.EvaluateJavaScript(
+                    "options.DisplayOptions.instance_"
+                    "         .displays_[%(index)d].resolutions"
+                    % {'index': display_index})
+        finally:
+            tab.Close()
+
+
+    def set_resolution(self, display_index, width, height, timeout=3):
+        """Sets the resolution of the specified display.
+
+        @param display_index: index of the display to set resolution for.
+        @param width: width of the resolution
+        @param height: height of the resolution
+        @param timeout: maximal time in seconds waiting for the new resolution
+                to settle in.
+        @raise TimeoutException when the operation is timed out.
+        """
+
+        tab = self._browser.tabs.New()
+        try:
+            tab.Navigate('chrome://settings-frame/display')
+            tab.Activate()
+            self._wait_for_display_options_to_appear(tab, display_index)
+
+            tab.ExecuteJavaScript(
+                    # Start from M38 (refer to CR:417113012), a DisplayMode dict
+                    # contains 'originalWidth'/'originalHeight' in addition to
+                    # 'width'/'height'. OriginalWidth/originalHeight is what is
+                    # supported by the display while width/height is what is
+                    # shown to users in the display setting.
+                    """
+                    var display = options.DisplayOptions.instance_
+                              .displays_[%(index)d];
+                    var modes = display.resolutions;
+                    var is_m38 = modes.length > 0
+                             && "originalWidth" in modes[0];
+                    if (is_m38) {
+                      for (index in modes) {
+                          var mode = modes[index];
+                          if (mode.originalWidth == %(width)d &&
+                                  mode.originalHeight == %(height)d) {
+                              chrome.send('setDisplayMode', [display.id, mode]);
+                              break;
+                          }
+                      }
+                    } else {
+                      chrome.send('setResolution',
+                          [display.id, %(width)d, %(height)d]);
+                    }
+                    """
+                    % {'index': display_index, 'width': width, 'height': height}
+            )
+
+            # TODO(tingyuan):
+            # Support for multiple external monitors (i.e. for chromebox)
+
+            end_time = time.time() + timeout
+            while time.time() < end_time:
+                r = self.get_resolution(self.get_external_connector_name())
+                if (width, height) == (r[0], r[1]):
+                    return True
+                time.sleep(0.1)
+            raise TimeoutException("Failed to change resolution to %r (%r"
+                    " detected)" % ((width, height), r))
+        finally:
+            tab.Close()
+
+
+    def get_resolution(self, output):
+        """Gets the resolution of the specified output.
+
+        @param output: The output name as a string.
+
+        @return The resolution of output as a tuple (width, height,
+            fb_offset_x, fb_offset_y) of ints.
+        """
+
+        regexp = re.compile(
+                r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)',
+                re.M)
+        match = regexp.findall(utils.call_xrandr())
+        for m in match:
+            if m[0] == output:
+                return (int(m[1]), int(m[2]), int(m[3]), int(m[4]))
+        return (0, 0, 0, 0)
+
+
+    def take_tab_screenshot(self, url_pattern, output_suffix):
+        """Takes a screenshot of the tab specified by the given url pattern.
+
+        The captured screenshot is saved to:
+            /tmp/screenshot_<output_suffix>_<last_part_of_url>.png
+
+        @param url_pattern: A string of url pattern used to search for tabs.
+        @param output_suffix: A suffix appended to the file name of captured
+                PNG image.
+        """
+        if not url_pattern:
+            # If no URL pattern is provided, defaults to capture all the tabs
+            # that show PNG images.
+            url_pattern = '.png'
+
+        tabs = self._browser.tabs
+        screenshots = []
+        for i in xrange(0, len(tabs)):
+            if url_pattern in tabs[i].url:
+                screenshots.append((tabs[i].url, tabs[i].Screenshot(timeout=5)))
+
+        output_file = ('/tmp/screenshot_%s_%%s.png' % output_suffix)
+        for url, screenshot in screenshots:
+            image_filename = os.path.splitext(url.rsplit('/', 1)[-1])[0]
+            screenshot.WriteFile(output_file % image_filename)
+        return True
+
+
+    def toggle_mirrored(self):
+        """Toggles mirrored.
+
+        Emulates L_Ctrl + Maximize in X server to toggle mirrored.
+        """
+        self.press_key('ctrl+F4')
+        return True
+
+
+    def press_key(self, key_str):
+        """Presses the given key(s).
+
+        @param key_str: A string of the key(s), like 'ctrl+F4', 'Up'.
+        """
+        command = 'xdotool key %s' % key_str
+        cros_ui.xsystem(command)
+        return True
+
+
+    def set_mirrored(self, is_mirrored):
+        """Sets mirrored mode.
+
+        @param is_mirrored: True or False to indicate mirrored state.
+        """
+        def _is_mirrored_enabled():
+            return bool(self.get_display_info()[0]['mirroringSourceId'])
+
+        retries = 3
+        while _is_mirrored_enabled() != is_mirrored and retries > 0:
+            self.toggle_mirrored()
+            time.sleep(3)
+            retries -= 1
+        return _is_mirrored_enabled() == is_mirrored
+
+
+    def suspend_resume(self, suspend_time=10):
+        """Suspends the DUT for a given time in second.
+
+        @param suspend_time: Suspend time in second.
+        """
+        sys_power.do_suspend(suspend_time)
+        return True
+
+
+    def suspend_resume_bg(self, suspend_time=10):
+        """Suspends the DUT for a given time in second in the background.
+
+        @param suspend_time: Suspend time in second.
+        """
+        process = multiprocessing.Process(target=self.suspend_resume,
+                                          args=(suspend_time,))
+        process.start()
+        return True
+
+
+    def get_external_connector_name(self):
+        """Gets the name of the external output connector.
+
+        @return The external output connector name as a string, if any.
+                Otherwise, return False.
+        """
+        xrandr_output = utils.get_xrandr_output_state()
+        for output in xrandr_output.iterkeys():
+            if (output.startswith('HDMI') or
+                output.startswith('DP') or
+                output.startswith('DVI')):
+                return output
+        return False
+
+
+    def get_internal_connector_name(self):
+        """Gets the name of the internal output connector.
+
+        @return The internal output connector name as a string, if any.
+                Otherwise, return False.
+        """
+        xrandr_output = utils.get_xrandr_output_state()
+        for output in xrandr_output.iterkeys():
+            # reference: chromium_org/chromeos/display/output_util.cc
+            if (output.startswith('eDP') or
+                output.startswith('LVDS') or
+                output.startswith('DSI')):
+                return output
+        return False
+
+
+    def wait_output_connected(self, output):
+        """Wait for output to connect.
+
+        @param output: The output name as a string.
+
+        @return: True if output is connected; False otherwise.
+        """
+        def _is_connected(output):
+            xrandr_output = utils.get_xrandr_output_state()
+            if output not in xrandr_output:
+                return False
+            return xrandr_output[output]
+        return utils.wait_for_value(lambda: _is_connected(output),
+                                    expected_value=True)
+
+
+    def load_url(self, url):
+        """Loads the given url in a new tab.
+
+        @param url: The url to load as a string.
+        """
+        tab = self._browser.tabs.New()
+        tab.Navigate(url)
+        tab.Activate()
+        return True
+
+
+    def close_tab(self, index=-1):
+        """Closes the tab of the given index.
+
+        @param index: The tab index to close. Defaults to the last tab.
+        """
+        self._browser.tabs[index].Close()
+        return True
+
+
+    def reconnect_output(self, output):
+        """Reconnects output.
+
+        @param output: The output name as a string.
+        """
+        utils.set_xrandr_output(output, False)
+        utils.set_xrandr_output(output, True)
+        return True