Tom Wai-Hong Tam | 1a197c9 | 2014-08-27 14:31:11 +0800 | [diff] [blame^] | 1 | # Copyright 2014 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Utility to access the display-related functionality.""" |
| 6 | |
| 7 | import multiprocessing |
| 8 | import os |
| 9 | import re |
| 10 | import time |
| 11 | import telemetry |
| 12 | |
| 13 | from autotest_lib.client.bin import utils |
| 14 | from autotest_lib.client.cros import constants, cros_ui, sys_power |
| 15 | |
| 16 | TimeoutException = telemetry.core.util.TimeoutException |
| 17 | |
| 18 | |
| 19 | class DisplayUtility(object): |
| 20 | """Utility to access the display-related functionality.""" |
| 21 | |
| 22 | def __init__(self, chrome): |
| 23 | self._chrome = chrome |
| 24 | self._browser = chrome.browser |
| 25 | |
| 26 | |
| 27 | def get_display_info(self): |
| 28 | """Gets the display info from Chrome.system.display API. |
| 29 | |
| 30 | @return array of dict for display info. |
| 31 | """ |
| 32 | |
| 33 | extension = self._chrome.get_extension( |
| 34 | constants.MULTIMEDIA_TEST_EXTENSION) |
| 35 | if not extension: |
| 36 | raise RuntimeError('Graphics test extension not found') |
| 37 | extension.ExecuteJavaScript('window.__display_info = null;') |
| 38 | extension.ExecuteJavaScript( |
| 39 | "chrome.system.display.getInfo(function(info) {" |
| 40 | "window.__display_info = info;})") |
| 41 | utils.wait_for_value(lambda: ( |
| 42 | extension.EvaluateJavaScript("window.__display_info") != None), |
| 43 | expected_value=True) |
| 44 | return extension.EvaluateJavaScript("window.__display_info") |
| 45 | |
| 46 | |
| 47 | def _wait_for_display_options_to_appear(self, tab, display_index, |
| 48 | timeout=16): |
| 49 | """Waits for option.DisplayOptions to appear. |
| 50 | |
| 51 | The function waits until options.DisplayOptions appears or is timed out |
| 52 | after the specified time. |
| 53 | |
| 54 | @param tab: the tab where the display options dialog is shown. |
| 55 | @param display_index: index of the display. |
| 56 | @param timeout: time wait for display options appear. |
| 57 | |
| 58 | @raise RuntimeError when display_index is out of range |
| 59 | @raise TimeoutException when the operation is timed out. |
| 60 | """ |
| 61 | |
| 62 | tab.WaitForJavaScriptExpression( |
| 63 | "typeof options !== 'undefined' &&" |
| 64 | "typeof options.DisplayOptions !== 'undefined' &&" |
| 65 | "typeof options.DisplayOptions.instance_ !== 'undefined' &&" |
| 66 | "typeof options.DisplayOptions.instance_" |
| 67 | " .displays_ !== 'undefined'", timeout) |
| 68 | |
| 69 | if not tab.EvaluateJavaScript( |
| 70 | "options.DisplayOptions.instance_.displays_.length > %d" |
| 71 | % (display_index)): |
| 72 | raise RuntimeError('Display index out of range: ' |
| 73 | + str(tab.EvaluateJavaScript( |
| 74 | "options.DisplayOptions.instance_.displays_.length"))) |
| 75 | |
| 76 | tab.WaitForJavaScriptExpression( |
| 77 | "typeof options.DisplayOptions.instance_" |
| 78 | " .displays_[%(index)d] !== 'undefined' &&" |
| 79 | "typeof options.DisplayOptions.instance_" |
| 80 | " .displays_[%(index)d].id !== 'undefined' &&" |
| 81 | "typeof options.DisplayOptions.instance_" |
| 82 | " .displays_[%(index)d].resolutions !== 'undefined'" |
| 83 | % {'index': display_index}, timeout) |
| 84 | |
| 85 | |
| 86 | def get_display_modes(self, display_index): |
| 87 | """Gets all the display modes for the specified display. |
| 88 | |
| 89 | The modes are obtained from chrome://settings-frame/display via |
| 90 | telemetry. |
| 91 | |
| 92 | @param display_index: index of the display to get modes from. |
| 93 | |
| 94 | @return: A list of DisplayMode dicts. |
| 95 | |
| 96 | @raise TimeoutException when the operation is timed out. |
| 97 | """ |
| 98 | |
| 99 | tab = self._browser.tabs.New() |
| 100 | try: |
| 101 | tab.Navigate('chrome://settings-frame/display') |
| 102 | tab.Activate() |
| 103 | self._wait_for_display_options_to_appear(tab, display_index) |
| 104 | return tab.EvaluateJavaScript( |
| 105 | "options.DisplayOptions.instance_" |
| 106 | " .displays_[%(index)d].resolutions" |
| 107 | % {'index': display_index}) |
| 108 | finally: |
| 109 | tab.Close() |
| 110 | |
| 111 | |
| 112 | def set_resolution(self, display_index, width, height, timeout=3): |
| 113 | """Sets the resolution of the specified display. |
| 114 | |
| 115 | @param display_index: index of the display to set resolution for. |
| 116 | @param width: width of the resolution |
| 117 | @param height: height of the resolution |
| 118 | @param timeout: maximal time in seconds waiting for the new resolution |
| 119 | to settle in. |
| 120 | @raise TimeoutException when the operation is timed out. |
| 121 | """ |
| 122 | |
| 123 | tab = self._browser.tabs.New() |
| 124 | try: |
| 125 | tab.Navigate('chrome://settings-frame/display') |
| 126 | tab.Activate() |
| 127 | self._wait_for_display_options_to_appear(tab, display_index) |
| 128 | |
| 129 | tab.ExecuteJavaScript( |
| 130 | # Start from M38 (refer to CR:417113012), a DisplayMode dict |
| 131 | # contains 'originalWidth'/'originalHeight' in addition to |
| 132 | # 'width'/'height'. OriginalWidth/originalHeight is what is |
| 133 | # supported by the display while width/height is what is |
| 134 | # shown to users in the display setting. |
| 135 | """ |
| 136 | var display = options.DisplayOptions.instance_ |
| 137 | .displays_[%(index)d]; |
| 138 | var modes = display.resolutions; |
| 139 | var is_m38 = modes.length > 0 |
| 140 | && "originalWidth" in modes[0]; |
| 141 | if (is_m38) { |
| 142 | for (index in modes) { |
| 143 | var mode = modes[index]; |
| 144 | if (mode.originalWidth == %(width)d && |
| 145 | mode.originalHeight == %(height)d) { |
| 146 | chrome.send('setDisplayMode', [display.id, mode]); |
| 147 | break; |
| 148 | } |
| 149 | } |
| 150 | } else { |
| 151 | chrome.send('setResolution', |
| 152 | [display.id, %(width)d, %(height)d]); |
| 153 | } |
| 154 | """ |
| 155 | % {'index': display_index, 'width': width, 'height': height} |
| 156 | ) |
| 157 | |
| 158 | # TODO(tingyuan): |
| 159 | # Support for multiple external monitors (i.e. for chromebox) |
| 160 | |
| 161 | end_time = time.time() + timeout |
| 162 | while time.time() < end_time: |
| 163 | r = self.get_resolution(self.get_external_connector_name()) |
| 164 | if (width, height) == (r[0], r[1]): |
| 165 | return True |
| 166 | time.sleep(0.1) |
| 167 | raise TimeoutException("Failed to change resolution to %r (%r" |
| 168 | " detected)" % ((width, height), r)) |
| 169 | finally: |
| 170 | tab.Close() |
| 171 | |
| 172 | |
| 173 | def get_resolution(self, output): |
| 174 | """Gets the resolution of the specified output. |
| 175 | |
| 176 | @param output: The output name as a string. |
| 177 | |
| 178 | @return The resolution of output as a tuple (width, height, |
| 179 | fb_offset_x, fb_offset_y) of ints. |
| 180 | """ |
| 181 | |
| 182 | regexp = re.compile( |
| 183 | r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)', |
| 184 | re.M) |
| 185 | match = regexp.findall(utils.call_xrandr()) |
| 186 | for m in match: |
| 187 | if m[0] == output: |
| 188 | return (int(m[1]), int(m[2]), int(m[3]), int(m[4])) |
| 189 | return (0, 0, 0, 0) |
| 190 | |
| 191 | |
| 192 | def take_tab_screenshot(self, url_pattern, output_suffix): |
| 193 | """Takes a screenshot of the tab specified by the given url pattern. |
| 194 | |
| 195 | The captured screenshot is saved to: |
| 196 | /tmp/screenshot_<output_suffix>_<last_part_of_url>.png |
| 197 | |
| 198 | @param url_pattern: A string of url pattern used to search for tabs. |
| 199 | @param output_suffix: A suffix appended to the file name of captured |
| 200 | PNG image. |
| 201 | """ |
| 202 | if not url_pattern: |
| 203 | # If no URL pattern is provided, defaults to capture all the tabs |
| 204 | # that show PNG images. |
| 205 | url_pattern = '.png' |
| 206 | |
| 207 | tabs = self._browser.tabs |
| 208 | screenshots = [] |
| 209 | for i in xrange(0, len(tabs)): |
| 210 | if url_pattern in tabs[i].url: |
| 211 | screenshots.append((tabs[i].url, tabs[i].Screenshot(timeout=5))) |
| 212 | |
| 213 | output_file = ('/tmp/screenshot_%s_%%s.png' % output_suffix) |
| 214 | for url, screenshot in screenshots: |
| 215 | image_filename = os.path.splitext(url.rsplit('/', 1)[-1])[0] |
| 216 | screenshot.WriteFile(output_file % image_filename) |
| 217 | return True |
| 218 | |
| 219 | |
| 220 | def toggle_mirrored(self): |
| 221 | """Toggles mirrored. |
| 222 | |
| 223 | Emulates L_Ctrl + Maximize in X server to toggle mirrored. |
| 224 | """ |
| 225 | self.press_key('ctrl+F4') |
| 226 | return True |
| 227 | |
| 228 | |
| 229 | def press_key(self, key_str): |
| 230 | """Presses the given key(s). |
| 231 | |
| 232 | @param key_str: A string of the key(s), like 'ctrl+F4', 'Up'. |
| 233 | """ |
| 234 | command = 'xdotool key %s' % key_str |
| 235 | cros_ui.xsystem(command) |
| 236 | return True |
| 237 | |
| 238 | |
| 239 | def set_mirrored(self, is_mirrored): |
| 240 | """Sets mirrored mode. |
| 241 | |
| 242 | @param is_mirrored: True or False to indicate mirrored state. |
| 243 | """ |
| 244 | def _is_mirrored_enabled(): |
| 245 | return bool(self.get_display_info()[0]['mirroringSourceId']) |
| 246 | |
| 247 | retries = 3 |
| 248 | while _is_mirrored_enabled() != is_mirrored and retries > 0: |
| 249 | self.toggle_mirrored() |
| 250 | time.sleep(3) |
| 251 | retries -= 1 |
| 252 | return _is_mirrored_enabled() == is_mirrored |
| 253 | |
| 254 | |
| 255 | def suspend_resume(self, suspend_time=10): |
| 256 | """Suspends the DUT for a given time in second. |
| 257 | |
| 258 | @param suspend_time: Suspend time in second. |
| 259 | """ |
| 260 | sys_power.do_suspend(suspend_time) |
| 261 | return True |
| 262 | |
| 263 | |
| 264 | def suspend_resume_bg(self, suspend_time=10): |
| 265 | """Suspends the DUT for a given time in second in the background. |
| 266 | |
| 267 | @param suspend_time: Suspend time in second. |
| 268 | """ |
| 269 | process = multiprocessing.Process(target=self.suspend_resume, |
| 270 | args=(suspend_time,)) |
| 271 | process.start() |
| 272 | return True |
| 273 | |
| 274 | |
| 275 | def get_external_connector_name(self): |
| 276 | """Gets the name of the external output connector. |
| 277 | |
| 278 | @return The external output connector name as a string, if any. |
| 279 | Otherwise, return False. |
| 280 | """ |
| 281 | xrandr_output = utils.get_xrandr_output_state() |
| 282 | for output in xrandr_output.iterkeys(): |
| 283 | if (output.startswith('HDMI') or |
| 284 | output.startswith('DP') or |
| 285 | output.startswith('DVI')): |
| 286 | return output |
| 287 | return False |
| 288 | |
| 289 | |
| 290 | def get_internal_connector_name(self): |
| 291 | """Gets the name of the internal output connector. |
| 292 | |
| 293 | @return The internal output connector name as a string, if any. |
| 294 | Otherwise, return False. |
| 295 | """ |
| 296 | xrandr_output = utils.get_xrandr_output_state() |
| 297 | for output in xrandr_output.iterkeys(): |
| 298 | # reference: chromium_org/chromeos/display/output_util.cc |
| 299 | if (output.startswith('eDP') or |
| 300 | output.startswith('LVDS') or |
| 301 | output.startswith('DSI')): |
| 302 | return output |
| 303 | return False |
| 304 | |
| 305 | |
| 306 | def wait_output_connected(self, output): |
| 307 | """Wait for output to connect. |
| 308 | |
| 309 | @param output: The output name as a string. |
| 310 | |
| 311 | @return: True if output is connected; False otherwise. |
| 312 | """ |
| 313 | def _is_connected(output): |
| 314 | xrandr_output = utils.get_xrandr_output_state() |
| 315 | if output not in xrandr_output: |
| 316 | return False |
| 317 | return xrandr_output[output] |
| 318 | return utils.wait_for_value(lambda: _is_connected(output), |
| 319 | expected_value=True) |
| 320 | |
| 321 | |
| 322 | def load_url(self, url): |
| 323 | """Loads the given url in a new tab. |
| 324 | |
| 325 | @param url: The url to load as a string. |
| 326 | """ |
| 327 | tab = self._browser.tabs.New() |
| 328 | tab.Navigate(url) |
| 329 | tab.Activate() |
| 330 | return True |
| 331 | |
| 332 | |
| 333 | def close_tab(self, index=-1): |
| 334 | """Closes the tab of the given index. |
| 335 | |
| 336 | @param index: The tab index to close. Defaults to the last tab. |
| 337 | """ |
| 338 | self._browser.tabs[index].Close() |
| 339 | return True |
| 340 | |
| 341 | |
| 342 | def reconnect_output(self, output): |
| 343 | """Reconnects output. |
| 344 | |
| 345 | @param output: The output name as a string. |
| 346 | """ |
| 347 | utils.set_xrandr_output(output, False) |
| 348 | utils.set_xrandr_output(output, True) |
| 349 | return True |