blob: 35420684888adb74735f0c980cca968dd3649e5d [file] [log] [blame]
Tom Wai-Hong Tam1a197c92014-08-27 14:31:11 +08001# 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"""Utility to access the display-related functionality."""
6
7import multiprocessing
8import os
9import re
10import time
11import telemetry
12
13from autotest_lib.client.bin import utils
14from autotest_lib.client.cros import constants, cros_ui, sys_power
15
16TimeoutException = telemetry.core.util.TimeoutException
17
18
19class DisplayUtility(object):
20 """Utility to access the display-related functionality."""
21
22 def __init__(self, chrome):
23 self._chrome = chrome
24 self._browser = chrome.browser
25
26
27 def get_display_info(self):
28 """Gets the display info from Chrome.system.display API.
29
30 @return array of dict for display info.
31 """
32
33 extension = self._chrome.get_extension(
34 constants.MULTIMEDIA_TEST_EXTENSION)
35 if not extension:
36 raise RuntimeError('Graphics test extension not found')
37 extension.ExecuteJavaScript('window.__display_info = null;')
38 extension.ExecuteJavaScript(
39 "chrome.system.display.getInfo(function(info) {"
40 "window.__display_info = info;})")
41 utils.wait_for_value(lambda: (
42 extension.EvaluateJavaScript("window.__display_info") != None),
43 expected_value=True)
44 return extension.EvaluateJavaScript("window.__display_info")
45
46
47 def _wait_for_display_options_to_appear(self, tab, display_index,
48 timeout=16):
49 """Waits for option.DisplayOptions to appear.
50
51 The function waits until options.DisplayOptions appears or is timed out
52 after the specified time.
53
54 @param tab: the tab where the display options dialog is shown.
55 @param display_index: index of the display.
56 @param timeout: time wait for display options appear.
57
58 @raise RuntimeError when display_index is out of range
59 @raise TimeoutException when the operation is timed out.
60 """
61
62 tab.WaitForJavaScriptExpression(
63 "typeof options !== 'undefined' &&"
64 "typeof options.DisplayOptions !== 'undefined' &&"
65 "typeof options.DisplayOptions.instance_ !== 'undefined' &&"
66 "typeof options.DisplayOptions.instance_"
67 " .displays_ !== 'undefined'", timeout)
68
69 if not tab.EvaluateJavaScript(
70 "options.DisplayOptions.instance_.displays_.length > %d"
71 % (display_index)):
72 raise RuntimeError('Display index out of range: '
73 + str(tab.EvaluateJavaScript(
74 "options.DisplayOptions.instance_.displays_.length")))
75
76 tab.WaitForJavaScriptExpression(
77 "typeof options.DisplayOptions.instance_"
78 " .displays_[%(index)d] !== 'undefined' &&"
79 "typeof options.DisplayOptions.instance_"
80 " .displays_[%(index)d].id !== 'undefined' &&"
81 "typeof options.DisplayOptions.instance_"
82 " .displays_[%(index)d].resolutions !== 'undefined'"
83 % {'index': display_index}, timeout)
84
85
86 def get_display_modes(self, display_index):
87 """Gets all the display modes for the specified display.
88
89 The modes are obtained from chrome://settings-frame/display via
90 telemetry.
91
92 @param display_index: index of the display to get modes from.
93
94 @return: A list of DisplayMode dicts.
95
96 @raise TimeoutException when the operation is timed out.
97 """
98
99 tab = self._browser.tabs.New()
100 try:
101 tab.Navigate('chrome://settings-frame/display')
102 tab.Activate()
103 self._wait_for_display_options_to_appear(tab, display_index)
104 return tab.EvaluateJavaScript(
105 "options.DisplayOptions.instance_"
106 " .displays_[%(index)d].resolutions"
107 % {'index': display_index})
108 finally:
109 tab.Close()
110
111
112 def set_resolution(self, display_index, width, height, timeout=3):
113 """Sets the resolution of the specified display.
114
115 @param display_index: index of the display to set resolution for.
116 @param width: width of the resolution
117 @param height: height of the resolution
118 @param timeout: maximal time in seconds waiting for the new resolution
119 to settle in.
120 @raise TimeoutException when the operation is timed out.
121 """
122
123 tab = self._browser.tabs.New()
124 try:
125 tab.Navigate('chrome://settings-frame/display')
126 tab.Activate()
127 self._wait_for_display_options_to_appear(tab, display_index)
128
129 tab.ExecuteJavaScript(
130 # Start from M38 (refer to CR:417113012), a DisplayMode dict
131 # contains 'originalWidth'/'originalHeight' in addition to
132 # 'width'/'height'. OriginalWidth/originalHeight is what is
133 # supported by the display while width/height is what is
134 # shown to users in the display setting.
135 """
136 var display = options.DisplayOptions.instance_
137 .displays_[%(index)d];
138 var modes = display.resolutions;
139 var is_m38 = modes.length > 0
140 && "originalWidth" in modes[0];
141 if (is_m38) {
142 for (index in modes) {
143 var mode = modes[index];
144 if (mode.originalWidth == %(width)d &&
145 mode.originalHeight == %(height)d) {
146 chrome.send('setDisplayMode', [display.id, mode]);
147 break;
148 }
149 }
150 } else {
151 chrome.send('setResolution',
152 [display.id, %(width)d, %(height)d]);
153 }
154 """
155 % {'index': display_index, 'width': width, 'height': height}
156 )
157
158 # TODO(tingyuan):
159 # Support for multiple external monitors (i.e. for chromebox)
160
161 end_time = time.time() + timeout
162 while time.time() < end_time:
163 r = self.get_resolution(self.get_external_connector_name())
164 if (width, height) == (r[0], r[1]):
165 return True
166 time.sleep(0.1)
167 raise TimeoutException("Failed to change resolution to %r (%r"
168 " detected)" % ((width, height), r))
169 finally:
170 tab.Close()
171
172
173 def get_resolution(self, output):
174 """Gets the resolution of the specified output.
175
176 @param output: The output name as a string.
177
178 @return The resolution of output as a tuple (width, height,
179 fb_offset_x, fb_offset_y) of ints.
180 """
181
182 regexp = re.compile(
183 r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)',
184 re.M)
185 match = regexp.findall(utils.call_xrandr())
186 for m in match:
187 if m[0] == output:
188 return (int(m[1]), int(m[2]), int(m[3]), int(m[4]))
189 return (0, 0, 0, 0)
190
191
192 def take_tab_screenshot(self, url_pattern, output_suffix):
193 """Takes a screenshot of the tab specified by the given url pattern.
194
195 The captured screenshot is saved to:
196 /tmp/screenshot_<output_suffix>_<last_part_of_url>.png
197
198 @param url_pattern: A string of url pattern used to search for tabs.
199 @param output_suffix: A suffix appended to the file name of captured
200 PNG image.
201 """
202 if not url_pattern:
203 # If no URL pattern is provided, defaults to capture all the tabs
204 # that show PNG images.
205 url_pattern = '.png'
206
207 tabs = self._browser.tabs
208 screenshots = []
209 for i in xrange(0, len(tabs)):
210 if url_pattern in tabs[i].url:
211 screenshots.append((tabs[i].url, tabs[i].Screenshot(timeout=5)))
212
213 output_file = ('/tmp/screenshot_%s_%%s.png' % output_suffix)
214 for url, screenshot in screenshots:
215 image_filename = os.path.splitext(url.rsplit('/', 1)[-1])[0]
216 screenshot.WriteFile(output_file % image_filename)
217 return True
218
219
220 def toggle_mirrored(self):
221 """Toggles mirrored.
222
223 Emulates L_Ctrl + Maximize in X server to toggle mirrored.
224 """
225 self.press_key('ctrl+F4')
226 return True
227
228
229 def press_key(self, key_str):
230 """Presses the given key(s).
231
232 @param key_str: A string of the key(s), like 'ctrl+F4', 'Up'.
233 """
234 command = 'xdotool key %s' % key_str
235 cros_ui.xsystem(command)
236 return True
237
238
239 def set_mirrored(self, is_mirrored):
240 """Sets mirrored mode.
241
242 @param is_mirrored: True or False to indicate mirrored state.
243 """
244 def _is_mirrored_enabled():
245 return bool(self.get_display_info()[0]['mirroringSourceId'])
246
247 retries = 3
248 while _is_mirrored_enabled() != is_mirrored and retries > 0:
249 self.toggle_mirrored()
250 time.sleep(3)
251 retries -= 1
252 return _is_mirrored_enabled() == is_mirrored
253
254
255 def suspend_resume(self, suspend_time=10):
256 """Suspends the DUT for a given time in second.
257
258 @param suspend_time: Suspend time in second.
259 """
260 sys_power.do_suspend(suspend_time)
261 return True
262
263
264 def suspend_resume_bg(self, suspend_time=10):
265 """Suspends the DUT for a given time in second in the background.
266
267 @param suspend_time: Suspend time in second.
268 """
269 process = multiprocessing.Process(target=self.suspend_resume,
270 args=(suspend_time,))
271 process.start()
272 return True
273
274
275 def get_external_connector_name(self):
276 """Gets the name of the external output connector.
277
278 @return The external output connector name as a string, if any.
279 Otherwise, return False.
280 """
281 xrandr_output = utils.get_xrandr_output_state()
282 for output in xrandr_output.iterkeys():
283 if (output.startswith('HDMI') or
284 output.startswith('DP') or
285 output.startswith('DVI')):
286 return output
287 return False
288
289
290 def get_internal_connector_name(self):
291 """Gets the name of the internal output connector.
292
293 @return The internal output connector name as a string, if any.
294 Otherwise, return False.
295 """
296 xrandr_output = utils.get_xrandr_output_state()
297 for output in xrandr_output.iterkeys():
298 # reference: chromium_org/chromeos/display/output_util.cc
299 if (output.startswith('eDP') or
300 output.startswith('LVDS') or
301 output.startswith('DSI')):
302 return output
303 return False
304
305
306 def wait_output_connected(self, output):
307 """Wait for output to connect.
308
309 @param output: The output name as a string.
310
311 @return: True if output is connected; False otherwise.
312 """
313 def _is_connected(output):
314 xrandr_output = utils.get_xrandr_output_state()
315 if output not in xrandr_output:
316 return False
317 return xrandr_output[output]
318 return utils.wait_for_value(lambda: _is_connected(output),
319 expected_value=True)
320
321
322 def load_url(self, url):
323 """Loads the given url in a new tab.
324
325 @param url: The url to load as a string.
326 """
327 tab = self._browser.tabs.New()
328 tab.Navigate(url)
329 tab.Activate()
330 return True
331
332
333 def close_tab(self, index=-1):
334 """Closes the tab of the given index.
335
336 @param index: The tab index to close. Defaults to the last tab.
337 """
338 self._browser.tabs[index].Close()
339 return True
340
341
342 def reconnect_output(self, output):
343 """Reconnects output.
344
345 @param output: The output name as a string.
346 """
347 utils.set_xrandr_output(output, False)
348 utils.set_xrandr_output(output, True)
349 return True