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