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 = 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