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