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