• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 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 CFM functionality."""
6
7import glob
8import logging
9import os
10import time
11import urlparse
12
13from autotest_lib.client.bin import utils
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib.cros import cfm_hangouts_api
16from autotest_lib.client.common_lib.cros import cfm_meetings_api
17from autotest_lib.client.common_lib.cros import enrollment
18from autotest_lib.client.common_lib.cros import kiosk_utils
19from autotest_lib.client.cros.graphics import graphics_utils
20
21
22class TimeoutException(Exception):
23    """Timeout Exception class."""
24    pass
25
26
27class CFMFacadeNative(object):
28    """Facade to access the CFM functionality.
29
30    The methods inside this class only accept Python native types.
31    """
32    _USER_ID = 'cr0s-cfm-la6-aut0t3st-us3r@croste.tv'
33    _PWD = 'test0000'
34    _EXT_ID = 'ikfcpmgefdpheiiomgmhlmmkihchmdlj'
35    _ENROLLMENT_DELAY = 15
36    _DEFAULT_TIMEOUT = 30
37
38    # Log file locations
39    _BASE_DIR = '/home/chronos/user/Storage/ext/'
40    _CALLGROK_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/0*/File System/000/t/00/0*'
41    _PA_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/def/File System/primary/p/00/0*'
42
43
44    def __init__(self, resource, screen):
45        """Initializes a CFMFacadeNative.
46
47        @param resource: A FacadeResource object.
48        """
49        self._resource = resource
50        self._screen = screen
51
52
53    def enroll_device(self):
54        """Enroll device into CFM."""
55        self._resource.start_custom_chrome({"auto_login": False,
56                                            "disable_gaia_services": False})
57        enrollment.RemoraEnrollment(self._resource._browser, self._USER_ID,
58                self._PWD)
59        # Timeout to allow for the device to stablize and go back to the
60        # login screen before proceeding.
61        time.sleep(self._ENROLLMENT_DELAY)
62
63
64    def restart_chrome_for_cfm(self):
65        """Restart chrome with custom values for CFM."""
66        custom_chrome_setup = {"clear_enterprise_policy": False,
67                               "dont_override_profile": True,
68                               "disable_gaia_services": False,
69                               "disable_default_apps": False,
70                               "auto_login": False}
71        self._resource.start_custom_chrome(custom_chrome_setup)
72
73
74    def check_hangout_extension_context(self):
75        """Check to make sure hangout app launched.
76
77        @raises error.TestFail if the URL checks fails.
78        """
79        ext_contexts = kiosk_utils.wait_for_kiosk_ext(
80                self._resource._browser, self._EXT_ID)
81        ext_urls = [context.EvaluateJavaScript('location.href;')
82                        for context in ext_contexts]
83        expected_urls = ['chrome-extension://' + self._EXT_ID + '/' + path
84                         for path in ['hangoutswindow.html?windowid=0',
85                                      'hangoutswindow.html?windowid=1',
86                                      'hangoutswindow.html?windowid=2',
87                                      '_generated_background_page.html']]
88        for url in ext_urls:
89            logging.info('Extension URL %s', url)
90            if url not in expected_urls:
91                raise error.TestFail(
92                    'Unexpected extension context urls, expected one of %s, '
93                    'got %s' % (expected_urls, url))
94
95
96    def take_screenshot(self, screenshot_name):
97        """
98        Takes a screenshot of what is currently displayed in png format.
99
100        The screenshot is stored in /tmp. Uses the low level graphics_utils API.
101
102        @param screenshot_name: Name of the screenshot file.
103        @returns The path to the screenshot or None.
104        """
105        try:
106            return graphics_utils.take_screenshot('/tmp', screenshot_name)
107        except Exception as e:
108            logging.warning('Taking screenshot failed', exc_info = e)
109            return None
110
111
112    def get_latest_callgrok_file_path(self):
113        """
114        @return The path to the lastest callgrok log file, if any.
115        """
116        try:
117            return max(glob.iglob(self._CALLGROK_LOGS_PATTERN),
118                       key=os.path.getctime)
119        except ValueError as e:
120            logging.exception('Error while searching for callgrok logs.')
121            return None
122
123
124    def get_latest_pa_logs_file_path(self):
125        """
126        @return The path to the lastest packaged app log file, if any.
127        """
128        try:
129            return max(glob.iglob(self._PA_LOGS_PATTERN), key=os.path.getctime)
130        except ValueError as e:
131            logging.exception('Error while searching for packaged app logs.')
132            return None
133
134
135    def reboot_device_with_chrome_api(self):
136        """Reboot device using chrome runtime API."""
137        ext_contexts = kiosk_utils.wait_for_kiosk_ext(
138                self._resource._browser, self._EXT_ID)
139        for context in ext_contexts:
140            context.WaitForDocumentReadyStateToBeInteractiveOrBetter()
141            ext_url = context.EvaluateJavaScript('document.URL')
142            background_url = ('chrome-extension://' + self._EXT_ID +
143                              '/_generated_background_page.html')
144            if ext_url in background_url:
145                context.ExecuteJavaScript('chrome.runtime.restart();')
146
147
148    def _get_webview_context_by_screen(self, screen):
149        """Get webview context that matches the screen param in the url.
150
151        @param screen: Value of the screen param, e.g. 'hotrod' or 'control'.
152        """
153        def _get_context():
154            try:
155                ctxs = kiosk_utils.get_webview_contexts(self._resource._browser,
156                                                        self._EXT_ID)
157                for ctx in ctxs:
158                    url_query = urlparse.urlparse(ctx.GetUrl()).query
159                    logging.info('Webview query: "%s"', url_query)
160                    params = urlparse.parse_qs(url_query,
161                                               keep_blank_values = True)
162                    is_oobe_slave_screen = ('nooobestatesync' in params and
163                                            'oobedone' in params)
164                    if is_oobe_slave_screen:
165                        # Skip the oobe slave screen. Not doing this can cause
166                        # the wrong webview context to be returned.
167                        continue
168                    if 'screen' in params and params['screen'][0] == screen:
169                        return ctx
170            except Exception as e:
171                # Having a MIMO attached to the DUT causes a couple of webview
172                # destruction/construction operations during OOBE. If we query a
173                # destructed webview it will throw an exception. Instead of
174                # failing the test, we just swallow the exception.
175                logging.exception(
176                    "Exception occured while querying the webview contexts.")
177            return None
178
179        return utils.poll_for_condition(
180                    _get_context,
181                    exception=error.TestFail(
182                        'Webview with screen param "%s" not found.' % screen),
183                    timeout=self._DEFAULT_TIMEOUT,
184                    sleep_interval = 1)
185
186
187    def skip_oobe_after_enrollment(self):
188        """Skips oobe and goes to the app landing page after enrollment."""
189        self.restart_chrome_for_cfm()
190        self.check_hangout_extension_context()
191        self.wait_for_hangouts_telemetry_commands()
192        self.wait_for_oobe_start_page()
193        self.skip_oobe_screen()
194
195
196    @property
197    def _webview_context(self):
198        """Get webview context object."""
199        return self._get_webview_context_by_screen(self._screen)
200
201
202    @property
203    def _cfmApi(self):
204        """Instantiate appropriate cfm api wrapper"""
205        if self._webview_context.EvaluateJavaScript(
206                "typeof window.hrRunDiagnosticsForTest == 'function'"):
207            return cfm_hangouts_api.CfmHangoutsAPI(self._webview_context)
208        if self._webview_context.EvaluateJavaScript(
209                "typeof window.hrTelemetryApi != 'undefined'"):
210            return cfm_meetings_api.CfmMeetingsAPI(self._webview_context)
211        raise error.TestFail('No hangouts or meet telemetry API available. '
212                             'Current url is "%s"' %
213                             self._webview_context.GetUrl())
214
215
216    #TODO: This is a legacy api. Deprecate this api and update existing hotrod
217    #      tests to use the new wait_for_hangouts_telemetry_commands api.
218    def wait_for_telemetry_commands(self):
219        """Wait for telemetry commands."""
220        self.wait_for_hangouts_telemetry_commands()
221
222
223    def wait_for_hangouts_telemetry_commands(self):
224        """Wait for Hangouts App telemetry commands."""
225        self._webview_context.WaitForJavaScriptCondition(
226                "typeof window.hrOobIsStartPageForTest == 'function'",
227                timeout=self._DEFAULT_TIMEOUT)
228
229
230    def wait_for_meetings_telemetry_commands(self):
231        """Wait for Meet App telemetry commands """
232        self._webview_context.WaitForJavaScriptCondition(
233                'window.hasOwnProperty("hrTelemetryApi")',
234                timeout=self._DEFAULT_TIMEOUT)
235
236
237    def wait_for_meetings_in_call_page(self):
238        """Waits for the in-call page to launch."""
239        self.wait_for_meetings_telemetry_commands()
240        self._cfmApi.wait_for_meetings_in_call_page()
241
242
243    def wait_for_meetings_landing_page(self):
244        """Waits for the landing page screen."""
245        self.wait_for_meetings_telemetry_commands()
246        self._cfmApi.wait_for_meetings_landing_page()
247
248
249    # UI commands/functions
250    def wait_for_oobe_start_page(self):
251        """Wait for oobe start screen to launch."""
252        self._cfmApi.wait_for_oobe_start_page()
253
254
255    def skip_oobe_screen(self):
256        """Skip Chromebox for Meetings oobe screen."""
257        self._cfmApi.skip_oobe_screen()
258
259
260    def is_oobe_start_page(self):
261        """Check if device is on CFM oobe start screen.
262
263        @return a boolean, based on oobe start page status.
264        """
265        return self._cfmApi.is_oobe_start_page()
266
267
268    # Hangouts commands/functions
269    def start_new_hangout_session(self, session_name):
270        """Start a new hangout session.
271
272        @param session_name: Name of the hangout session.
273        """
274        self._cfmApi.start_new_hangout_session(session_name)
275
276
277    def end_hangout_session(self):
278        """End current hangout session."""
279        self._cfmApi.end_hangout_session()
280
281
282    def is_in_hangout_session(self):
283        """Check if device is in hangout session.
284
285        @return a boolean, for hangout session state.
286        """
287        return self._cfmApi.is_in_hangout_session()
288
289
290    def is_ready_to_start_hangout_session(self):
291        """Check if device is ready to start a new hangout session.
292
293        @return a boolean for hangout session ready state.
294        """
295        return self._cfmApi.is_ready_to_start_hangout_session()
296
297
298    def join_meeting_session(self, session_name):
299        """Joins a meeting.
300
301        @param session_name: Name of the meeting session.
302        """
303        self._cfmApi.join_meeting_session(session_name)
304
305
306    def start_meeting_session(self):
307        """Start a meeting."""
308        self._cfmApi.start_meeting_session()
309
310
311    def end_meeting_session(self):
312        """End current meeting session."""
313        self._cfmApi.end_meeting_session()
314
315
316    def get_participant_count(self):
317        """Gets the total participant count in a call."""
318        return self._cfmApi.get_participant_count()
319
320
321    # Diagnostics commands/functions
322    def is_diagnostic_run_in_progress(self):
323        """Check if hotrod diagnostics is running.
324
325        @return a boolean for diagnostic run state.
326        """
327        return self._cfmApi.is_diagnostic_run_in_progress()
328
329
330    def wait_for_diagnostic_run_to_complete(self):
331        """Wait for hotrod diagnostics to complete."""
332        self._cfmApi.wait_for_diagnostic_run_to_complete()
333
334
335    def run_diagnostics(self):
336        """Run hotrod diagnostics."""
337        self._cfmApi.run_diagnostics()
338
339
340    def get_last_diagnostics_results(self):
341        """Get latest hotrod diagnostics results.
342
343        @return a dict with diagnostic test results.
344        """
345        return self._cfmApi.get_last_diagnostics_results()
346
347
348    # Mic audio commands/functions
349    def is_mic_muted(self):
350        """Check if mic is muted.
351
352        @return a boolean for mic mute state.
353        """
354        return self._cfmApi.is_mic_muted()
355
356
357    def mute_mic(self):
358        """Local mic mute from toolbar."""
359        self._cfmApi.mute_mic()
360
361
362    def unmute_mic(self):
363        """Local mic unmute from toolbar."""
364        self._cfmApi.unmute_mic()
365
366
367    def remote_mute_mic(self):
368        """Remote mic mute request from cPanel."""
369        self._cfmApi.remote_mute_mic()
370
371
372    def remote_unmute_mic(self):
373        """Remote mic unmute request from cPanel."""
374        self._cfmApi.remote_unmute_mic()
375
376
377    def get_mic_devices(self):
378        """Get all mic devices detected by hotrod.
379
380        @return a list of mic devices.
381        """
382        return self._cfmApi.get_mic_devices()
383
384
385    def get_preferred_mic(self):
386        """Get mic preferred for hotrod.
387
388        @return a str with preferred mic name.
389        """
390        return self._cfmApi.get_preferred_mic()
391
392
393    def set_preferred_mic(self, mic):
394        """Set preferred mic for hotrod.
395
396        @param mic: String with mic name.
397        """
398        self._cfmApi.set_preferred_mic(mic)
399
400
401    # Speaker commands/functions
402    def get_speaker_devices(self):
403        """Get all speaker devices detected by hotrod.
404
405        @return a list of speaker devices.
406        """
407        return self._cfmApi.get_speaker_devices()
408
409
410    def get_preferred_speaker(self):
411        """Get speaker preferred for hotrod.
412
413        @return a str with preferred speaker name.
414        """
415        return self._cfmApi.get_preferred_speaker()
416
417
418    def set_preferred_speaker(self, speaker):
419        """Set preferred speaker for hotrod.
420
421        @param speaker: String with speaker name.
422        """
423        self._cfmApi.set_preferred_speaker(speaker)
424
425
426    def set_speaker_volume(self, volume_level):
427        """Set speaker volume.
428
429        @param volume_level: String value ranging from 0-100 to set volume to.
430        """
431        self._cfmApi.set_speaker_volume(volume_level)
432
433
434    def get_speaker_volume(self):
435        """Get current speaker volume.
436
437        @return a str value with speaker volume level 0-100.
438        """
439        return self._cfmApi.get_speaker_volume()
440
441
442    def play_test_sound(self):
443        """Play test sound."""
444        self._cfmApi.play_test_sound()
445
446
447    # Camera commands/functions
448    def get_camera_devices(self):
449        """Get all camera devices detected by hotrod.
450
451        @return a list of camera devices.
452        """
453        return self._cfmApi.get_camera_devices()
454
455
456    def get_preferred_camera(self):
457        """Get camera preferred for hotrod.
458
459        @return a str with preferred camera name.
460        """
461        return self._cfmApi.get_preferred_camera()
462
463
464    def set_preferred_camera(self, camera):
465        """Set preferred camera for hotrod.
466
467        @param camera: String with camera name.
468        """
469        self._cfmApi.set_preferred_camera(camera)
470
471
472    def is_camera_muted(self):
473        """Check if camera is muted (turned off).
474
475        @return a boolean for camera muted state.
476        """
477        return self._cfmApi.is_camera_muted()
478
479
480    def mute_camera(self):
481        """Turned camera off."""
482        self._cfmApi.mute_camera()
483
484
485    def unmute_camera(self):
486        """Turned camera on."""
487        self._cfmApi.unmute_camera()
488
489    def move_camera(self, camera_motion):
490        """Move camera(PTZ commands).
491
492        @param camera_motion: Set of allowed commands
493            defined in cfmApi.move_camera.
494        """
495        self._cfmApi.move_camera(camera_motion)
496
497    def get_media_info_data_points(self):
498        """
499        Gets media info data points containing media stats.
500
501        These are exported on the window object when the
502        ExportMediaInfo mod is enabled.
503
504        @returns A list with dictionaries of media info data points.
505        @raises RuntimeError if the data point API is not available.
506        """
507        is_api_available_script = (
508                '"realtime" in window '
509                '&& "media" in realtime '
510                '&& "getMediaInfoDataPoints" in realtime.media')
511        if not self._webview_context.EvaluateJavaScript(
512                is_api_available_script):
513            raise RuntimeError(
514                    'realtime.media.getMediaInfoDataPoints not available. '
515                    'Is the ExportMediaInfo mod active? '
516                    'The mod is only available for Meet.')
517
518        data_points = self._webview_context.EvaluateJavaScript(
519                'window.realtime.media.getMediaInfoDataPoints()')
520        for data_point in data_points:
521            # XML RCP gives overflow errors when trying to send too large
522            # integers or longs. Convert timestamps to float seconds and media
523            # stats to floats. We do not care if we lose some precision.
524            # When we are at it, convert the timestamp to seconds as
525            # expected in Python.
526            data_point['timestamp'] = data_point['timestamp'] / 1000.0
527            for media in data_point['media']:
528                for k, v in media.iteritems():
529                    if type(v) == int:
530                        media[k] = float(v)
531        return data_points
532
533