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