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