display_facade_native.py revision 055ada6014f9c5808c616a2d1417b3261e2e1b10
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"""Facade to access the display-related functionality.""" 6 7import exceptions 8import multiprocessing 9import os 10import re 11import time 12import telemetry 13 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.common_lib.cros import retry 17from autotest_lib.client.cros import constants, sys_power 18from autotest_lib.client.cros.graphics import graphics_utils 19from autotest_lib.client.cros.multimedia import image_generator 20 21TimeoutException = telemetry.core.util.TimeoutException 22 23 24_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60 25_FLAKY_CALL_RETRY_DELAY_SEC = 1 26 27_telemetry_devtools = telemetry.core.backends.chrome_inspector.devtools_http 28_retry_chrome_call = retry.retry( 29 (telemetry.core.exceptions.BrowserConnectionGoneException, 30 telemetry.core.exceptions.DevtoolsTargetCrashException, 31 _telemetry_devtools.DevToolsClientUrlError, 32 exceptions.IndexError), 33 timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0, 34 delay_sec=_FLAKY_CALL_RETRY_DELAY_SEC) 35 36_retry_display_call = retry.retry( 37 (KeyError, error.CmdError), 38 timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0, 39 delay_sec=_FLAKY_CALL_RETRY_DELAY_SEC) 40 41 42class DisplayFacadeNative(object): 43 """Facade to access the display-related functionality. 44 45 The methods inside this class only accept Python native types. 46 """ 47 48 CALIBRATION_IMAGE_PATH = '/tmp/calibration.svg' 49 50 def __init__(self, chrome): 51 self._chrome = chrome 52 self._browser = chrome.browser 53 self._image_generator = image_generator.ImageGenerator() 54 55 56 @_retry_chrome_call 57 def get_display_info(self): 58 """Gets the display info from Chrome.system.display API. 59 60 @return array of dict for display info. 61 """ 62 63 extension = self._chrome.get_extension( 64 constants.MULTIMEDIA_TEST_EXTENSION) 65 if not extension: 66 raise RuntimeError('Graphics test extension not found') 67 extension.ExecuteJavaScript('window.__display_info = null;') 68 extension.ExecuteJavaScript( 69 "chrome.system.display.getInfo(function(info) {" 70 "window.__display_info = info;})") 71 utils.wait_for_value(lambda: ( 72 extension.EvaluateJavaScript("window.__display_info") != None), 73 expected_value=True) 74 return extension.EvaluateJavaScript("window.__display_info") 75 76 77 def _wait_for_display_options_to_appear(self, tab, display_index, 78 timeout=16): 79 """Waits for option.DisplayOptions to appear. 80 81 The function waits until options.DisplayOptions appears or is timed out 82 after the specified time. 83 84 @param tab: the tab where the display options dialog is shown. 85 @param display_index: index of the display. 86 @param timeout: time wait for display options appear. 87 88 @raise RuntimeError when display_index is out of range 89 @raise TimeoutException when the operation is timed out. 90 """ 91 92 tab.WaitForJavaScriptExpression( 93 "typeof options !== 'undefined' &&" 94 "typeof options.DisplayOptions !== 'undefined' &&" 95 "typeof options.DisplayOptions.instance_ !== 'undefined' &&" 96 "typeof options.DisplayOptions.instance_" 97 " .displays_ !== 'undefined'", timeout) 98 99 if not tab.EvaluateJavaScript( 100 "options.DisplayOptions.instance_.displays_.length > %d" 101 % (display_index)): 102 raise RuntimeError('Display index out of range: ' 103 + str(tab.EvaluateJavaScript( 104 "options.DisplayOptions.instance_.displays_.length"))) 105 106 tab.WaitForJavaScriptExpression( 107 "typeof options.DisplayOptions.instance_" 108 " .displays_[%(index)d] !== 'undefined' &&" 109 "typeof options.DisplayOptions.instance_" 110 " .displays_[%(index)d].id !== 'undefined' &&" 111 "typeof options.DisplayOptions.instance_" 112 " .displays_[%(index)d].resolutions !== 'undefined'" 113 % {'index': display_index}, timeout) 114 115 116 def get_display_modes(self, display_index): 117 """Gets all the display modes for the specified display. 118 119 The modes are obtained from chrome://settings-frame/display via 120 telemetry. 121 122 @param display_index: index of the display to get modes from. 123 124 @return: A list of DisplayMode dicts. 125 126 @raise TimeoutException when the operation is timed out. 127 """ 128 try: 129 tab = self._load_url('chrome://settings-frame/display') 130 self._wait_for_display_options_to_appear(tab, display_index) 131 return tab.EvaluateJavaScript( 132 "options.DisplayOptions.instance_" 133 " .displays_[%(index)d].resolutions" 134 % {'index': display_index}) 135 finally: 136 self.close_tab() 137 138 139 def get_available_resolutions(self, display_index): 140 """Gets the resolutions from the specified display. 141 142 @return a list of (width, height) tuples. 143 """ 144 # Start from M38 (refer to http://codereview.chromium.org/417113012), 145 # a DisplayMode dict contains 'originalWidth'/'originalHeight' 146 # in addition to 'width'/'height'. 147 # OriginalWidth/originalHeight is what is supported by the display 148 # while width/height is what is shown to users in the display setting. 149 modes = self.get_display_modes(display_index) 150 if modes: 151 if 'originalWidth' in modes[0]: 152 # M38 or newer 153 # TODO(tingyuan): fix loading image for cases where original 154 # width/height is different from width/height. 155 return list(set([(mode['originalWidth'], mode['originalHeight']) 156 for mode in modes])) 157 158 # pre-M38 159 return [(mode['width'], mode['height']) for mode in modes 160 if 'scale' not in mode] 161 162 163 def get_first_external_display_index(self): 164 """Gets the first external display index. 165 166 @return the index of the first external display; False if not found. 167 """ 168 # Get the first external and enabled display 169 for index, display in enumerate(self.get_display_info()): 170 if display['isEnabled'] and not display['isInternal']: 171 return index 172 return False 173 174 175 def set_resolution(self, display_index, width, height, timeout=3): 176 """Sets the resolution of the specified display. 177 178 @param display_index: index of the display to set resolution for. 179 @param width: width of the resolution 180 @param height: height of the resolution 181 @param timeout: maximal time in seconds waiting for the new resolution 182 to settle in. 183 @raise TimeoutException when the operation is timed out. 184 """ 185 186 try: 187 tab = self._load_url('chrome://settings-frame/display') 188 self._wait_for_display_options_to_appear(tab, display_index) 189 190 tab.ExecuteJavaScript( 191 # Start from M38 (refer to CR:417113012), a DisplayMode dict 192 # contains 'originalWidth'/'originalHeight' in addition to 193 # 'width'/'height'. OriginalWidth/originalHeight is what is 194 # supported by the display while width/height is what is 195 # shown to users in the display setting. 196 """ 197 var display = options.DisplayOptions.instance_ 198 .displays_[%(index)d]; 199 var modes = display.resolutions; 200 var is_m38 = modes.length > 0 201 && "originalWidth" in modes[0]; 202 if (is_m38) { 203 for (index in modes) { 204 var mode = modes[index]; 205 if (mode.originalWidth == %(width)d && 206 mode.originalHeight == %(height)d) { 207 chrome.send('setDisplayMode', [display.id, mode]); 208 break; 209 } 210 } 211 } else { 212 chrome.send('setResolution', 213 [display.id, %(width)d, %(height)d]); 214 } 215 """ 216 % {'index': display_index, 'width': width, 'height': height} 217 ) 218 219 # TODO(tingyuan): 220 # Support for multiple external monitors (i.e. for chromebox) 221 222 end_time = time.time() + timeout 223 while time.time() < end_time: 224 r = self.get_output_rect(self.get_external_connector_name()) 225 if (width, height) == (r[0], r[1]): 226 return True 227 time.sleep(0.1) 228 raise TimeoutException("Failed to change resolution to %r (%r" 229 " detected)" % ((width, height), r)) 230 finally: 231 self.close_tab() 232 233 234 @_retry_display_call 235 def get_output_rect(self, output): 236 """Gets the size and position of the given output on the screen buffer. 237 238 @param output: The output name as a string. 239 240 @return A tuple of the rectangle (width, height, fb_offset_x, 241 fb_offset_y) of ints. 242 """ 243 regexp = re.compile( 244 r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)', 245 re.M) 246 match = regexp.findall(graphics_utils.call_xrandr()) 247 for m in match: 248 if m[0] == output: 249 return (int(m[1]), int(m[2]), int(m[3]), int(m[4])) 250 return (0, 0, 0, 0) 251 252 253 def get_external_resolution(self): 254 """Gets the resolution of the external screen. 255 256 @return The resolution tuple (width, height) 257 """ 258 connector = self.get_external_connector_name() 259 width, height, _, _ = self.get_output_rect(connector) 260 return (width, height) 261 262 263 def get_internal_resolution(self): 264 """Gets the resolution of the internal screen. 265 266 @return The resolution tuple (width, height) 267 """ 268 connector = self.get_internal_connector_name() 269 width, height, _, _ = self.get_output_rect(connector) 270 return (width, height) 271 272 273 def set_content_protection(self, state): 274 """Sets the content protection of the external screen. 275 276 @param state: One of the states 'Undesired', 'Desired', or 'Enabled' 277 """ 278 connector = self.get_external_connector_name() 279 graphics_utils.set_content_protection(connector, state) 280 281 282 def get_content_protection(self): 283 """Gets the state of the content protection. 284 285 @param output: The output name as a string. 286 @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'. 287 False if not supported. 288 """ 289 connector = self.get_external_connector_name() 290 return graphics_utils.get_content_protection(connector) 291 292 293 def take_screenshot_crop(self, path, box): 294 """Captures the DUT screenshot, use box for cropping. 295 296 @param path: path to image file. 297 @param box: 4-tuple giving the upper left and lower right coordinates. 298 """ 299 graphics_utils.take_screenshot_crop(path, box) 300 return True 301 302 303 def take_tab_screenshot(self, url_pattern, output_suffix): 304 """Takes a screenshot of the tab specified by the given url pattern. 305 306 The captured screenshot is saved to: 307 /tmp/screenshot_<output_suffix>_<last_part_of_url>.png 308 309 @param url_pattern: A string of url pattern used to search for tabs. 310 @param output_suffix: A suffix appended to the file name of captured 311 PNG image. 312 """ 313 if not url_pattern: 314 # If no URL pattern is provided, defaults to capture all the tabs 315 # that show PNG images. 316 url_pattern = '.png' 317 318 tabs = self._browser.tabs 319 screenshots = [] 320 for i in xrange(0, len(tabs)): 321 if url_pattern in tabs[i].url: 322 screenshots.append((tabs[i].url, tabs[i].Screenshot(timeout=5))) 323 324 output_file = ('/tmp/screenshot_%s_%%s.png' % output_suffix) 325 for url, screenshot in screenshots: 326 image_filename = os.path.splitext(url.rsplit('/', 1)[-1])[0] 327 screenshot.WriteFile(output_file % image_filename) 328 return True 329 330 331 def toggle_mirrored(self): 332 """Toggles mirrored.""" 333 graphics_utils.screen_toggle_mirrored() 334 return True 335 336 337 def hide_cursor(self): 338 """Hides mouse cursor.""" 339 graphics_utils.hide_cursor() 340 return True 341 342 343 def is_mirrored_enabled(self): 344 """Checks the mirrored state. 345 346 @return True if mirrored mode is enabled. 347 """ 348 return bool(self.get_display_info()[0]['mirroringSourceId']) 349 350 351 def set_mirrored(self, is_mirrored): 352 """Sets mirrored mode. 353 354 @param is_mirrored: True or False to indicate mirrored state. 355 """ 356 retries = 3 357 while self.is_mirrored_enabled() != is_mirrored and retries > 0: 358 self.toggle_mirrored() 359 time.sleep(3) 360 retries -= 1 361 return self.is_mirrored_enabled() == is_mirrored 362 363 364 def is_display_primary(self, internal=True): 365 """Checks if internal screen is primary display. 366 367 @param internal: is internal/external screen primary status requested 368 @return boolean True if internal display is primary. 369 """ 370 for info in self.get_display_info(): 371 if info['isInternal'] == internal and info['isPrimary']: 372 return True 373 return False 374 375 376 def suspend_resume(self, suspend_time=10): 377 """Suspends the DUT for a given time in second. 378 379 @param suspend_time: Suspend time in second. 380 """ 381 sys_power.do_suspend(suspend_time) 382 return True 383 384 385 def suspend_resume_bg(self, suspend_time=10): 386 """Suspends the DUT for a given time in second in the background. 387 388 @param suspend_time: Suspend time in second. 389 """ 390 process = multiprocessing.Process(target=self.suspend_resume, 391 args=(suspend_time,)) 392 process.start() 393 return True 394 395 396 @_retry_display_call 397 def get_external_connector_name(self): 398 """Gets the name of the external output connector. 399 400 @return The external output connector name as a string, if any. 401 Otherwise, return False. 402 """ 403 return graphics_utils.get_external_connector_name() 404 405 406 def get_internal_connector_name(self): 407 """Gets the name of the internal output connector. 408 409 @return The internal output connector name as a string, if any. 410 Otherwise, return False. 411 """ 412 return graphics_utils.get_internal_connector_name() 413 414 415 def wait_external_display_connected(self, display): 416 """Waits for the specified external display to be connected. 417 418 @param display: The display name as a string, like 'HDMI1', or 419 False if no external display is expected. 420 @return: True if display is connected; False otherwise. 421 """ 422 result = utils.wait_for_value(self.get_external_connector_name, 423 expected_value=display) 424 return result == display 425 426 427 @_retry_chrome_call 428 def _load_url(self, url): 429 """Loads the given url in a new tab. 430 431 @param url: The url to load as a string. 432 @return: A new tab object. 433 """ 434 tab = self._browser.tabs.New() 435 tab.Navigate(url) 436 tab.Activate() 437 return tab 438 439 440 def load_calibration_image(self, resolution): 441 """Load a full screen calibration image from the HTTP server. 442 443 @param resolution: A tuple (width, height) of resolution. 444 """ 445 path = self.CALIBRATION_IMAGE_PATH 446 self._image_generator.generate_image(resolution[0], resolution[1], path) 447 os.chmod(path, 0644) 448 self._load_url('file://%s' % path) 449 return True 450 451 452 @_retry_chrome_call 453 def close_tab(self, index=-1): 454 """Closes the tab of the given index. 455 456 @param index: The tab index to close. Defaults to the last tab. 457 """ 458 self._browser.tabs[index].Close() 459 return True 460