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