Home | History | Annotate | Download | only in multimedia
      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 
      7 import glob
      8 import logging
      9 import os
     10 import time
     11 import urlparse
     12 
     13 from autotest_lib.client.bin import utils
     14 from autotest_lib.client.common_lib import error
     15 from autotest_lib.client.common_lib.cros import cfm_hangouts_api
     16 from autotest_lib.client.common_lib.cros import cfm_meetings_api
     17 from autotest_lib.client.common_lib.cros import enrollment
     18 from autotest_lib.client.common_lib.cros import kiosk_utils
     19 from autotest_lib.client.cros.graphics import graphics_utils
     20 
     21 
     22 class TimeoutException(Exception):
     23     """Timeout Exception class."""
     24     pass
     25 
     26 
     27 class 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 (at] 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