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