• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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