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