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