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