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