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