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