display_facade_native.py revision 10a1c6a6773d2f42ee27e396ecf318b7bac457b3
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 numpy 10import os 11import re 12import time 13from autotest_lib.client.bin import utils 14from autotest_lib.client.common_lib import error 15from autotest_lib.client.common_lib.cros import chrome, retry 16from autotest_lib.client.cros import constants, sys_power 17from autotest_lib.client.cros.graphics import graphics_utils 18from autotest_lib.client.cros.multimedia import image_generator 19 20class TimeoutException(Exception): 21 pass 22 23 24_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60 25_FLAKY_CHROME_CALL_RETRY_DELAY_SEC = 1 26_FLAKY_DISPLAY_CALL_RETRY_DELAY_SEC = 2 27 28_retry_chrome_call = retry.retry( 29 (chrome.Error, exceptions.IndexError), 30 timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0, 31 delay_sec=_FLAKY_CHROME_CALL_RETRY_DELAY_SEC) 32 33_retry_display_call = retry.retry( 34 (KeyError, error.CmdError), 35 timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0, 36 delay_sec=_FLAKY_DISPLAY_CALL_RETRY_DELAY_SEC) 37 38 39class DisplayFacadeNative(object): 40 """Facade to access the display-related functionality. 41 42 The methods inside this class only accept Python native types. 43 """ 44 45 CALIBRATION_IMAGE_PATH = '/tmp/calibration.svg' 46 47 def __init__(self, chrome): 48 self._chrome = chrome 49 self._browser = chrome.browser 50 self._image_generator = image_generator.ImageGenerator() 51 52 53 @_retry_chrome_call 54 def get_display_info(self): 55 """Gets the display info from Chrome.system.display API. 56 57 @return array of dict for display info. 58 """ 59 60 extension = self._chrome.get_extension( 61 constants.DISPLAY_TEST_EXTENSION) 62 if not extension: 63 raise RuntimeError('Display test extension not found') 64 extension.ExecuteJavaScript('window.__display_info = null;') 65 extension.ExecuteJavaScript( 66 "chrome.system.display.getInfo(function(info) {" 67 "window.__display_info = info;})") 68 utils.wait_for_value(lambda: ( 69 extension.EvaluateJavaScript("window.__display_info") != None), 70 expected_value=True) 71 return extension.EvaluateJavaScript("window.__display_info") 72 73 74 @_retry_chrome_call 75 def get_window_info(self): 76 """Gets the current window info from Chrome.system.window API. 77 78 @return a dict for the information of the current window. 79 """ 80 81 extension = self._chrome.autotest_ext 82 if not extension: 83 raise RuntimeError('Autotest extension not found') 84 extension.ExecuteJavaScript('window.__window_info = null;') 85 extension.ExecuteJavaScript( 86 "chrome.windows.getCurrent(function(info) {" 87 "window.__window_info = info;})") 88 utils.wait_for_value(lambda: ( 89 extension.EvaluateJavaScript("window.__window_info") != None), 90 expected_value=True) 91 return extension.EvaluateJavaScript("window.__window_info") 92 93 94 def _wait_for_display_options_to_appear(self, tab, display_index, 95 timeout=16): 96 """Waits for option.DisplayOptions to appear. 97 98 The function waits until options.DisplayOptions appears or is timed out 99 after the specified time. 100 101 @param tab: the tab where the display options dialog is shown. 102 @param display_index: index of the display. 103 @param timeout: time wait for display options appear. 104 105 @raise RuntimeError when display_index is out of range 106 @raise TimeoutException when the operation is timed out. 107 """ 108 109 tab.WaitForJavaScriptExpression( 110 "typeof options !== 'undefined' &&" 111 "typeof options.DisplayOptions !== 'undefined' &&" 112 "typeof options.DisplayOptions.instance_ !== 'undefined' &&" 113 "typeof options.DisplayOptions.instance_" 114 " .displays_ !== 'undefined'", timeout) 115 116 if not tab.EvaluateJavaScript( 117 "options.DisplayOptions.instance_.displays_.length > %d" 118 % (display_index)): 119 raise RuntimeError('Display index out of range: ' 120 + str(tab.EvaluateJavaScript( 121 "options.DisplayOptions.instance_.displays_.length"))) 122 123 tab.WaitForJavaScriptExpression( 124 "typeof options.DisplayOptions.instance_" 125 " .displays_[%(index)d] !== 'undefined' &&" 126 "typeof options.DisplayOptions.instance_" 127 " .displays_[%(index)d].id !== 'undefined' &&" 128 "typeof options.DisplayOptions.instance_" 129 " .displays_[%(index)d].resolutions !== 'undefined'" 130 % {'index': display_index}, timeout) 131 132 133 def get_display_modes(self, display_index): 134 """Gets all the display modes for the specified display. 135 136 The modes are obtained from chrome://settings-frame/display via 137 telemetry. 138 139 @param display_index: index of the display to get modes from. 140 141 @return: A list of DisplayMode dicts. 142 143 @raise TimeoutException when the operation is timed out. 144 """ 145 try: 146 tab = self._load_url('chrome://settings-frame/display') 147 self._wait_for_display_options_to_appear(tab, display_index) 148 return tab.EvaluateJavaScript( 149 "options.DisplayOptions.instance_" 150 " .displays_[%(index)d].resolutions" 151 % {'index': display_index}) 152 finally: 153 self.close_tab() 154 155 156 def get_available_resolutions(self, display_index): 157 """Gets the resolutions from the specified display. 158 159 @return a list of (width, height) tuples. 160 """ 161 # Start from M38 (refer to http://codereview.chromium.org/417113012), 162 # a DisplayMode dict contains 'originalWidth'/'originalHeight' 163 # in addition to 'width'/'height'. 164 # OriginalWidth/originalHeight is what is supported by the display 165 # while width/height is what is shown to users in the display setting. 166 modes = self.get_display_modes(display_index) 167 if modes: 168 if 'originalWidth' in modes[0]: 169 # M38 or newer 170 # TODO(tingyuan): fix loading image for cases where original 171 # width/height is different from width/height. 172 return list(set([(mode['originalWidth'], mode['originalHeight']) 173 for mode in modes])) 174 175 # pre-M38 176 return [(mode['width'], mode['height']) for mode in modes 177 if 'scale' not in mode] 178 179 180 def get_first_external_display_index(self): 181 """Gets the first external display index. 182 183 @return the index of the first external display; False if not found. 184 """ 185 # Get the first external and enabled display 186 for index, display in enumerate(self.get_display_info()): 187 if display['isEnabled'] and not display['isInternal']: 188 return index 189 return False 190 191 192 def set_resolution(self, display_index, width, height, timeout=3): 193 """Sets the resolution of the specified display. 194 195 @param display_index: index of the display to set resolution for. 196 @param width: width of the resolution 197 @param height: height of the resolution 198 @param timeout: maximal time in seconds waiting for the new resolution 199 to settle in. 200 @raise TimeoutException when the operation is timed out. 201 """ 202 203 try: 204 tab = self._load_url('chrome://settings-frame/display') 205 self._wait_for_display_options_to_appear(tab, display_index) 206 207 tab.ExecuteJavaScript( 208 # Start from M38 (refer to CR:417113012), a DisplayMode dict 209 # contains 'originalWidth'/'originalHeight' in addition to 210 # 'width'/'height'. OriginalWidth/originalHeight is what is 211 # supported by the display while width/height is what is 212 # shown to users in the display setting. 213 """ 214 var display = options.DisplayOptions.instance_ 215 .displays_[%(index)d]; 216 var modes = display.resolutions; 217 for (index in modes) { 218 var mode = modes[index]; 219 if (mode.originalWidth == %(width)d && 220 mode.originalHeight == %(height)d) { 221 chrome.send('setDisplayMode', [display.id, mode]); 222 break; 223 } 224 } 225 """ 226 % {'index': display_index, 'width': width, 'height': height} 227 ) 228 229 def _get_selected_resolution(): 230 modes = tab.EvaluateJavaScript( 231 """ 232 options.DisplayOptions.instance_ 233 .displays_[%(index)d].resolutions 234 """ 235 % {'index': display_index}) 236 for mode in modes: 237 if mode['selected']: 238 return (mode['originalWidth'], mode['originalHeight']) 239 240 # TODO(tingyuan): 241 # Support for multiple external monitors (i.e. for chromebox) 242 end_time = time.time() + timeout 243 while time.time() < end_time: 244 r = _get_selected_resolution() 245 if (width, height) == (r[0], r[1]): 246 return True 247 time.sleep(0.1) 248 raise TimeoutException('Failed to change resolution to %r (%r' 249 ' detected)' % ((width, height), r)) 250 finally: 251 self.close_tab() 252 253 254 @_retry_display_call 255 def get_external_resolution(self): 256 """Gets the resolution of the external screen. 257 258 @return The resolution tuple (width, height) 259 """ 260 return graphics_utils.get_external_resolution() 261 262 def get_internal_resolution(self): 263 """Gets the resolution of the internal screen. 264 265 @return The resolution tuple (width, height) or None if internal screen 266 is not available 267 """ 268 for display in self.get_display_info(): 269 if display['isInternal']: 270 bounds = display['bounds'] 271 return (bounds['width'], bounds['height']) 272 return None 273 274 275 def set_content_protection(self, state): 276 """Sets the content protection of the external screen. 277 278 @param state: One of the states 'Undesired', 'Desired', or 'Enabled' 279 """ 280 connector = self.get_external_connector_name() 281 graphics_utils.set_content_protection(connector, state) 282 283 284 def get_content_protection(self): 285 """Gets the state of the content protection. 286 287 @param output: The output name as a string. 288 @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'. 289 False if not supported. 290 """ 291 connector = self.get_external_connector_name() 292 return graphics_utils.get_content_protection(connector) 293 294 295 def get_external_crtc(self): 296 """Gets the external crtc. 297 298 @return The id of the external crtc.""" 299 return graphics_utils.get_external_crtc() 300 301 302 def get_internal_crtc(self): 303 """Gets the internal crtc. 304 305 @retrun The id of the internal crtc.""" 306 return graphics_utils.get_internal_crtc() 307 308 309 def get_output_rect(self, output): 310 """Gets the size and position of the given output on the screen buffer. 311 312 @param output: The output name as a string. 313 314 @return A tuple of the rectangle (width, height, fb_offset_x, 315 fb_offset_y) of ints. 316 """ 317 regexp = re.compile( 318 r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)', 319 re.M) 320 match = regexp.findall(graphics_utils.call_xrandr()) 321 for m in match: 322 if m[0] == output: 323 return (int(m[1]), int(m[2]), int(m[3]), int(m[4])) 324 return (0, 0, 0, 0) 325 326 327 def take_internal_screenshot(self, path): 328 if utils.is_freon(): 329 self.take_screenshot_crtc(path, self.get_internal_crtc()) 330 else: 331 output = self.get_internal_connector_name() 332 box = self.get_output_rect(output) 333 graphics_utils.take_screenshot_crop_x(path, box) 334 return output, box # for logging/debugging 335 336 337 def take_external_screenshot(self, path): 338 if utils.is_freon(): 339 self.take_screenshot_crtc(path, self.get_external_crtc()) 340 else: 341 output = self.get_external_connector_name() 342 box = self.get_output_rect(output) 343 graphics_utils.take_screenshot_crop_x(path, box) 344 return output, box # for logging/debugging 345 346 347 def take_screenshot_crtc(self, path, id): 348 """Captures the DUT screenshot, use id for selecting screen. 349 350 @param path: path to image file. 351 @param id: The id of the crtc to screenshot. 352 """ 353 354 graphics_utils.take_screenshot_crop(path, crtc_id=id) 355 return True 356 357 358 def take_tab_screenshot(self, output_path, url_pattern=None): 359 """Takes a screenshot of the tab specified by the given url pattern. 360 361 @param output_path: A path of the output file. 362 @param url_pattern: A string of url pattern used to search for tabs. 363 Default is to look for .svg image. 364 """ 365 if url_pattern is None: 366 # If no URL pattern is provided, defaults to capture the first 367 # tab that shows SVG image. 368 url_pattern = '.svg' 369 370 tabs = self._browser.tabs 371 for i in xrange(0, len(tabs)): 372 if url_pattern in tabs[i].url: 373 data = tabs[i].Screenshot(timeout=5) 374 # Flip the colors from BGR to RGB. 375 data = numpy.fliplr(data.reshape(-1, 3)).reshape(data.shape) 376 data.tofile(output_path) 377 break 378 return True 379 380 381 def toggle_mirrored(self): 382 """Toggles mirrored.""" 383 graphics_utils.screen_toggle_mirrored() 384 return True 385 386 387 def hide_cursor(self): 388 """Hides mouse cursor.""" 389 graphics_utils.hide_cursor() 390 return True 391 392 393 def is_mirrored_enabled(self): 394 """Checks the mirrored state. 395 396 @return True if mirrored mode is enabled. 397 """ 398 return bool(self.get_display_info()[0]['mirroringSourceId']) 399 400 401 def set_mirrored(self, is_mirrored): 402 """Sets mirrored mode. 403 404 @param is_mirrored: True or False to indicate mirrored state. 405 @return True if success, False otherwise. 406 """ 407 # TODO: Do some experiments to minimize waiting time after toggling. 408 retries = 3 409 while self.is_mirrored_enabled() != is_mirrored and retries > 0: 410 self.toggle_mirrored() 411 time.sleep(3) 412 retries -= 1 413 return self.is_mirrored_enabled() == is_mirrored 414 415 416 def is_display_primary(self, internal=True): 417 """Checks if internal screen is primary display. 418 419 @param internal: is internal/external screen primary status requested 420 @return boolean True if internal display is primary. 421 """ 422 for info in self.get_display_info(): 423 if info['isInternal'] == internal and info['isPrimary']: 424 return True 425 return False 426 427 428 def suspend_resume(self, suspend_time=10): 429 """Suspends the DUT for a given time in second. 430 431 @param suspend_time: Suspend time in second. 432 """ 433 sys_power.do_suspend(suspend_time) 434 return True 435 436 437 def suspend_resume_bg(self, suspend_time=10): 438 """Suspends the DUT for a given time in second in the background. 439 440 @param suspend_time: Suspend time in second. 441 """ 442 process = multiprocessing.Process(target=self.suspend_resume, 443 args=(suspend_time,)) 444 process.start() 445 return True 446 447 448 @_retry_display_call 449 def get_external_connector_name(self): 450 """Gets the name of the external output connector. 451 452 @return The external output connector name as a string, if any. 453 Otherwise, return False. 454 """ 455 return graphics_utils.get_external_connector_name() 456 457 458 def get_internal_connector_name(self): 459 """Gets the name of the internal output connector. 460 461 @return The internal output connector name as a string, if any. 462 Otherwise, return False. 463 """ 464 return graphics_utils.get_internal_connector_name() 465 466 467 def wait_external_display_connected(self, display): 468 """Waits for the specified external display to be connected. 469 470 @param display: The display name as a string, like 'HDMI1', or 471 False if no external display is expected. 472 @return: True if display is connected; False otherwise. 473 """ 474 result = utils.wait_for_value(self.get_external_connector_name, 475 expected_value=display) 476 return result == display 477 478 479 @_retry_chrome_call 480 def move_to_display(self, display_index): 481 """Moves the current window to the indicated display. 482 483 @param display_index: The index of the indicated display. 484 @return True if success, False otherwise. 485 """ 486 display_info = self.get_display_info() 487 if (display_index is False or 488 display_index not in xrange(0, len(display_info)) or 489 not display_info[display_index]['isEnabled']): 490 raise RuntimeError('Cannot find the indicated display') 491 target_bounds = display_info[display_index]['bounds'] 492 extension = self._chrome.autotest_ext 493 if not extension: 494 raise RuntimeError('Autotest extension not found') 495 # If the area of bounds is empty (here we achieve this by setting 496 # width and height to zero), the window_sizer will automatically 497 # determine an area which is visible and fits on the screen. 498 # For more details, see chrome/browser/ui/window_sizer.cc 499 # Without setting state to 'normal', if the current state is 500 # 'minimized', 'maximized' or 'fullscreen', the setting of 501 # 'left', 'top', 'width' and 'height' will be ignored. 502 # For more details, see chrome/browser/extensions/api/tabs/tabs_api.cc 503 extension.ExecuteJavaScript( 504 """ 505 var __status = 'Running'; 506 chrome.windows.update( 507 chrome.windows.WINDOW_ID_CURRENT, 508 {left: %d, top: %d, width: 0, height: 0, 509 state: 'normal'}, 510 function() { __status = 'Done'; }); 511 """ 512 % (target_bounds['left'], target_bounds['top']) 513 ) 514 utils.wait_for_value(lambda: ( 515 extension.EvaluateJavaScript('__status') == 'Done'), 516 expected_value=True) 517 return extension.EvaluateJavaScript('__status') == 'Done' 518 519 520 def toggle_fullscreen(self): 521 """Toggles mirrored.""" 522 graphics_utils.screen_toggle_fullscreen() 523 return True 524 525 526 def is_fullscreen_enabled(self): 527 """Checks the fullscreen state. 528 529 @return True if fullscreen mode is enabled. 530 """ 531 return self.get_window_info()['state'] == 'fullscreen' 532 533 534 def set_fullscreen(self, is_fullscreen): 535 """Sets the current window to full screen. 536 537 @param is_fullscreen: True or False to indicate fullscreen state. 538 @return True if success, False otherwise. 539 """ 540 # TODO: Do some experiments to minimize waiting time after toggling. 541 retries = 3 542 while self.is_fullscreen_enabled() != is_fullscreen and retries > 0: 543 self.toggle_fullscreen() 544 time.sleep(3) 545 retries -= 1 546 return self.is_fullscreen_enabled() == is_fullscreen 547 548 549 @_retry_chrome_call 550 def _load_url(self, url): 551 """Loads the given url in a new tab. 552 553 @param url: The url to load as a string. 554 @return: A new tab object. 555 """ 556 tab = self._browser.tabs.New() 557 tab.Navigate(url) 558 tab.Activate() 559 return tab 560 561 562 def load_calibration_image(self, resolution): 563 """Load a full screen calibration image from the HTTP server. 564 565 @param resolution: A tuple (width, height) of resolution. 566 """ 567 path = self.CALIBRATION_IMAGE_PATH 568 self._image_generator.generate_image(resolution[0], resolution[1], path) 569 os.chmod(path, 0644) 570 self._load_url('file://%s' % path) 571 return True 572 573 574 @_retry_chrome_call 575 def close_tab(self, index=-1): 576 """Disables fullscreen and closes the tab of the given index. 577 578 @param index: The tab index to close. Defaults to the last tab. 579 """ 580 # set_fullscreen(False) is necessary here because currently there 581 # is a bug in tabs.Close(). If the current state is fullscreen and 582 # we call close_tab() without setting state back to normal, it will 583 # cancel fullscreen mode without changing system configuration, and 584 # so that the next time someone calls set_fullscreen(True), the 585 # function will find that current state is already 'fullscreen' 586 # (though it is not) and do nothing, which will break all the 587 # following tests. 588 self.set_fullscreen(False) 589 self._browser.tabs[index].Close() 590 return True 591