xref: /aosp_15_r20/external/autotest/client/cros/multimedia/cfm_facade.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright 2017 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li"""Facade to access the CFM functionality."""
7*9c5db199SXin Li
8*9c5db199SXin Lifrom __future__ import absolute_import
9*9c5db199SXin Lifrom __future__ import division
10*9c5db199SXin Lifrom __future__ import print_function
11*9c5db199SXin Li
12*9c5db199SXin Liimport glob
13*9c5db199SXin Liimport logging
14*9c5db199SXin Liimport os
15*9c5db199SXin Liimport time
16*9c5db199SXin Liimport six
17*9c5db199SXin Liimport six.moves.urllib.parse
18*9c5db199SXin Liimport six.moves.xmlrpc_client
19*9c5db199SXin Li
20*9c5db199SXin Lifrom autotest_lib.client.bin import utils
21*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
22*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import cfm_hangouts_api
23*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import cfm_meetings_api
24*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import enrollment
25*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import kiosk_utils
26*9c5db199SXin Lifrom autotest_lib.client.cros.graphics import graphics_utils
27*9c5db199SXin Li
28*9c5db199SXin Li
29*9c5db199SXin Liclass TimeoutException(Exception):
30*9c5db199SXin Li    """Timeout Exception class."""
31*9c5db199SXin Li    pass
32*9c5db199SXin Li
33*9c5db199SXin Li
34*9c5db199SXin Liclass CFMFacadeLocal(object):
35*9c5db199SXin Li    """Facade to access the CFM functionality.
36*9c5db199SXin Li
37*9c5db199SXin Li    The methods inside this class only accept Python native types.
38*9c5db199SXin Li    """
39*9c5db199SXin Li    _USER_ID = '[email protected]'
40*9c5db199SXin Li    _PWD = 'test0000'
41*9c5db199SXin Li    _EXT_ID = 'ikfcpmgefdpheiiomgmhlmmkihchmdlj'
42*9c5db199SXin Li    _ENROLLMENT_DELAY = 45
43*9c5db199SXin Li    _DEFAULT_TIMEOUT = 30
44*9c5db199SXin Li
45*9c5db199SXin Li    # Log file locations
46*9c5db199SXin Li    _BASE_DIR = '/home/chronos/user/Storage/ext/'
47*9c5db199SXin Li    _CALLGROK_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/0*/File System/000/t/00/0*'
48*9c5db199SXin Li    _PA_LOGS_PATTERN = _BASE_DIR + _EXT_ID + '/def/File System/primary/p/00/0*'
49*9c5db199SXin Li
50*9c5db199SXin Li
51*9c5db199SXin Li    def __init__(self, resource, screen):
52*9c5db199SXin Li        """Initializes a CFMFacadeLocal.
53*9c5db199SXin Li
54*9c5db199SXin Li        @param resource: A FacadeResource object.
55*9c5db199SXin Li        """
56*9c5db199SXin Li        self._resource = resource
57*9c5db199SXin Li        self._screen = screen
58*9c5db199SXin Li
59*9c5db199SXin Li
60*9c5db199SXin Li    def enroll_device(self):
61*9c5db199SXin Li        """Enroll device into CFM."""
62*9c5db199SXin Li        logging.info('Enrolling device...')
63*9c5db199SXin Li        extra_browser_args = ["--force-devtools-available"]
64*9c5db199SXin Li        self._resource.start_custom_chrome({
65*9c5db199SXin Li            "auto_login": False,
66*9c5db199SXin Li            "disable_gaia_services": False,
67*9c5db199SXin Li            "extra_browser_args": extra_browser_args})
68*9c5db199SXin Li
69*9c5db199SXin Li        enrollment.RemoraEnrollment(self._resource._browser, self._USER_ID,
70*9c5db199SXin Li                self._PWD)
71*9c5db199SXin Li        # Timeout to allow for the device to stablize and go back to the
72*9c5db199SXin Li        # OOB screen before proceeding. The device may restart the app a couple
73*9c5db199SXin Li        # of times before it reaches the OOB screen.
74*9c5db199SXin Li        time.sleep(self._ENROLLMENT_DELAY)
75*9c5db199SXin Li        logging.info('Enrollment completed.')
76*9c5db199SXin Li
77*9c5db199SXin Li
78*9c5db199SXin Li    def restart_chrome_for_cfm(self, extra_chrome_args=None):
79*9c5db199SXin Li        """Restart chrome with custom values for CFM.
80*9c5db199SXin Li
81*9c5db199SXin Li        @param extra_chrome_args a list with extra command line arguments for
82*9c5db199SXin Li                Chrome.
83*9c5db199SXin Li        """
84*9c5db199SXin Li        logging.info('Restarting chrome for CfM...')
85*9c5db199SXin Li        custom_chrome_setup = {"clear_enterprise_policy": False,
86*9c5db199SXin Li                               "dont_override_profile": True,
87*9c5db199SXin Li                               "disable_gaia_services": False,
88*9c5db199SXin Li                               "disable_default_apps": False,
89*9c5db199SXin Li                               "auto_login": False}
90*9c5db199SXin Li        custom_chrome_setup["extra_browser_args"] = (
91*9c5db199SXin Li            ["--force-devtools-available"])
92*9c5db199SXin Li        if extra_chrome_args:
93*9c5db199SXin Li            custom_chrome_setup["extra_browser_args"].extend(extra_chrome_args)
94*9c5db199SXin Li        self._resource.start_custom_chrome(custom_chrome_setup)
95*9c5db199SXin Li        logging.info('Chrome process restarted in CfM mode.')
96*9c5db199SXin Li
97*9c5db199SXin Li
98*9c5db199SXin Li    def check_hangout_extension_context(self):
99*9c5db199SXin Li        """Check to make sure hangout app launched.
100*9c5db199SXin Li
101*9c5db199SXin Li        @raises error.TestFail if the URL checks fails.
102*9c5db199SXin Li        """
103*9c5db199SXin Li        logging.info('Verifying extension contexts...')
104*9c5db199SXin Li        ext_contexts = kiosk_utils.wait_for_kiosk_ext(
105*9c5db199SXin Li                self._resource._browser, self._EXT_ID)
106*9c5db199SXin Li        ext_urls = [context.EvaluateJavaScript('location.href;')
107*9c5db199SXin Li                        for context in ext_contexts]
108*9c5db199SXin Li        expected_urls = ['chrome-extension://' + self._EXT_ID + '/' + path
109*9c5db199SXin Li                         for path in ['hangoutswindow.html?windowid=0',
110*9c5db199SXin Li                                      'hangoutswindow.html?windowid=1',
111*9c5db199SXin Li                                      'hangoutswindow.html?windowid=2',
112*9c5db199SXin Li                                      '_generated_background_page.html']]
113*9c5db199SXin Li        for url in ext_urls:
114*9c5db199SXin Li            logging.info('Extension URL %s', url)
115*9c5db199SXin Li            if url not in expected_urls:
116*9c5db199SXin Li                raise error.TestFail(
117*9c5db199SXin Li                    'Unexpected extension context urls, expected one of %s, '
118*9c5db199SXin Li                    'got %s' % (expected_urls, url))
119*9c5db199SXin Li        logging.info('Hangouts extension contexts verified.')
120*9c5db199SXin Li
121*9c5db199SXin Li
122*9c5db199SXin Li    def take_screenshot(self, screenshot_name):
123*9c5db199SXin Li        """
124*9c5db199SXin Li        Takes a screenshot of what is currently displayed in png format.
125*9c5db199SXin Li
126*9c5db199SXin Li        The screenshot is stored in /tmp. Uses the low level graphics_utils API.
127*9c5db199SXin Li
128*9c5db199SXin Li        @param screenshot_name: Name of the screenshot file.
129*9c5db199SXin Li        @returns The path to the screenshot or None.
130*9c5db199SXin Li        """
131*9c5db199SXin Li        try:
132*9c5db199SXin Li            return graphics_utils.take_screenshot('/tmp', screenshot_name)
133*9c5db199SXin Li        except Exception as e:
134*9c5db199SXin Li            logging.warning('Taking screenshot failed', exc_info = e)
135*9c5db199SXin Li            return None
136*9c5db199SXin Li
137*9c5db199SXin Li
138*9c5db199SXin Li    def get_latest_callgrok_file_path(self):
139*9c5db199SXin Li        """
140*9c5db199SXin Li        @return The path to the lastest callgrok log file, if any.
141*9c5db199SXin Li        """
142*9c5db199SXin Li        try:
143*9c5db199SXin Li            return max(glob.iglob(self._CALLGROK_LOGS_PATTERN),
144*9c5db199SXin Li                       key=os.path.getctime)
145*9c5db199SXin Li        except ValueError as e:
146*9c5db199SXin Li            logging.exception('Error while searching for callgrok logs.')
147*9c5db199SXin Li            return None
148*9c5db199SXin Li
149*9c5db199SXin Li
150*9c5db199SXin Li    def get_latest_pa_logs_file_path(self):
151*9c5db199SXin Li        """
152*9c5db199SXin Li        @return The path to the lastest packaged app log file, if any.
153*9c5db199SXin Li        """
154*9c5db199SXin Li        try:
155*9c5db199SXin Li            return max(self.get_all_pa_logs_file_path(), key=os.path.getctime)
156*9c5db199SXin Li        except ValueError as e:
157*9c5db199SXin Li            logging.exception('Error while searching for packaged app logs.')
158*9c5db199SXin Li            return None
159*9c5db199SXin Li
160*9c5db199SXin Li
161*9c5db199SXin Li    def get_all_pa_logs_file_path(self):
162*9c5db199SXin Li        """
163*9c5db199SXin Li        @return The paths to the all packaged app log files, if any.
164*9c5db199SXin Li        """
165*9c5db199SXin Li        return glob.glob(self._PA_LOGS_PATTERN)
166*9c5db199SXin Li
167*9c5db199SXin Li    def reboot_device_with_chrome_api(self):
168*9c5db199SXin Li        """Reboot device using chrome runtime API."""
169*9c5db199SXin Li        ext_contexts = kiosk_utils.wait_for_kiosk_ext(
170*9c5db199SXin Li                self._resource._browser, self._EXT_ID)
171*9c5db199SXin Li        for context in ext_contexts:
172*9c5db199SXin Li            context.WaitForDocumentReadyStateToBeInteractiveOrBetter()
173*9c5db199SXin Li            ext_url = context.EvaluateJavaScript('document.URL')
174*9c5db199SXin Li            background_url = ('chrome-extension://' + self._EXT_ID +
175*9c5db199SXin Li                              '/_generated_background_page.html')
176*9c5db199SXin Li            if ext_url in background_url:
177*9c5db199SXin Li                context.ExecuteJavaScript('chrome.runtime.restart();')
178*9c5db199SXin Li
179*9c5db199SXin Li
180*9c5db199SXin Li    def _get_webview_context_by_screen(self, screen):
181*9c5db199SXin Li        """Get webview context that matches the screen param in the url.
182*9c5db199SXin Li
183*9c5db199SXin Li        @param screen: Value of the screen param, e.g. 'hotrod' or 'control'.
184*9c5db199SXin Li        """
185*9c5db199SXin Li        def _get_context():
186*9c5db199SXin Li            try:
187*9c5db199SXin Li                ctxs = kiosk_utils.get_webview_contexts(self._resource._browser,
188*9c5db199SXin Li                                                        self._EXT_ID)
189*9c5db199SXin Li                for ctx in ctxs:
190*9c5db199SXin Li                    parse_result = six.moves.urllib.parse.urlparse(ctx.GetUrl())
191*9c5db199SXin Li                    url_path = parse_result.path
192*9c5db199SXin Li                    logging.info('Webview path: "%s"', url_path)
193*9c5db199SXin Li                    url_query = parse_result.query
194*9c5db199SXin Li                    logging.info('Webview query: "%s"', url_query)
195*9c5db199SXin Li                    params = six.moves.urllib.parse.parse_qs(url_query,
196*9c5db199SXin Li                                               keep_blank_values = True)
197*9c5db199SXin Li                    is_oobe_node_screen = (
198*9c5db199SXin Li                        # Hangouts Classic
199*9c5db199SXin Li                        ('nooobestatesync' in params and 'oobedone' in params)
200*9c5db199SXin Li                        # Hangouts Meet
201*9c5db199SXin Li                        or ('oobesecondary' in url_path))
202*9c5db199SXin Li                    if is_oobe_node_screen:
203*9c5db199SXin Li                        # Skip the oobe node screen. Not doing this can cause
204*9c5db199SXin Li                        # the wrong webview context to be returned.
205*9c5db199SXin Li                        continue
206*9c5db199SXin Li                    if 'screen' in params and params['screen'][0] == screen:
207*9c5db199SXin Li                        return ctx
208*9c5db199SXin Li            except Exception as e:
209*9c5db199SXin Li                # Having a MIMO attached to the DUT causes a couple of webview
210*9c5db199SXin Li                # destruction/construction operations during OOBE. If we query a
211*9c5db199SXin Li                # destructed webview it will throw an exception. Instead of
212*9c5db199SXin Li                # failing the test, we just swallow the exception.
213*9c5db199SXin Li                logging.exception(
214*9c5db199SXin Li                    "Exception occured while querying the webview contexts.")
215*9c5db199SXin Li            return None
216*9c5db199SXin Li
217*9c5db199SXin Li        return utils.poll_for_condition(
218*9c5db199SXin Li                    _get_context,
219*9c5db199SXin Li                    exception=error.TestFail(
220*9c5db199SXin Li                        'Webview with screen param "%s" not found.' % screen),
221*9c5db199SXin Li                    timeout=self._DEFAULT_TIMEOUT,
222*9c5db199SXin Li                    sleep_interval = 1)
223*9c5db199SXin Li
224*9c5db199SXin Li
225*9c5db199SXin Li    def skip_oobe_after_enrollment(self):
226*9c5db199SXin Li        """Skips oobe and goes to the app landing page after enrollment."""
227*9c5db199SXin Li        # Due to a variying amount of app restarts before we reach the OOB page
228*9c5db199SXin Li        # we need to restart Chrome in order to make sure we have the devtools
229*9c5db199SXin Li        # handle available and up-to-date.
230*9c5db199SXin Li        self.restart_chrome_for_cfm()
231*9c5db199SXin Li        self.check_hangout_extension_context()
232*9c5db199SXin Li        self.wait_for_telemetry_commands()
233*9c5db199SXin Li        self.wait_for_oobe_start_page()
234*9c5db199SXin Li        self.skip_oobe_screen()
235*9c5db199SXin Li
236*9c5db199SXin Li
237*9c5db199SXin Li    @property
238*9c5db199SXin Li    def _webview_context(self):
239*9c5db199SXin Li        """Get webview context object."""
240*9c5db199SXin Li        return self._get_webview_context_by_screen(self._screen)
241*9c5db199SXin Li
242*9c5db199SXin Li
243*9c5db199SXin Li    @property
244*9c5db199SXin Li    def _cfmApi(self):
245*9c5db199SXin Li        """Instantiate appropriate cfm api wrapper"""
246*9c5db199SXin Li        if self._webview_context.EvaluateJavaScript(
247*9c5db199SXin Li                "typeof window.hrRunDiagnosticsForTest == 'function'"):
248*9c5db199SXin Li            return cfm_hangouts_api.CfmHangoutsAPI(self._webview_context)
249*9c5db199SXin Li        if self._webview_context.EvaluateJavaScript(
250*9c5db199SXin Li                "typeof window.hrTelemetryApi != 'undefined'"):
251*9c5db199SXin Li            return cfm_meetings_api.CfmMeetingsAPI(self._webview_context)
252*9c5db199SXin Li        raise error.TestFail('No hangouts or meet telemetry API available. '
253*9c5db199SXin Li                             'Current url is "%s"' %
254*9c5db199SXin Li                             self._webview_context.GetUrl())
255*9c5db199SXin Li
256*9c5db199SXin Li
257*9c5db199SXin Li    def wait_for_telemetry_commands(self):
258*9c5db199SXin Li        """Wait for telemetry commands."""
259*9c5db199SXin Li        logging.info('Wait for Hangouts telemetry commands')
260*9c5db199SXin Li        self._webview_context.WaitForJavaScriptCondition(
261*9c5db199SXin Li            """typeof window.hrOobIsStartPageForTest == 'function'
262*9c5db199SXin Li               || typeof window.hrTelemetryApi != 'undefined'
263*9c5db199SXin Li            """,
264*9c5db199SXin Li            timeout=self._DEFAULT_TIMEOUT)
265*9c5db199SXin Li
266*9c5db199SXin Li
267*9c5db199SXin Li    def wait_for_meetings_in_call_page(self):
268*9c5db199SXin Li        """Waits for the in-call page to launch."""
269*9c5db199SXin Li        self.wait_for_telemetry_commands()
270*9c5db199SXin Li        self._cfmApi.wait_for_meetings_in_call_page()
271*9c5db199SXin Li
272*9c5db199SXin Li
273*9c5db199SXin Li    def wait_for_meetings_landing_page(self):
274*9c5db199SXin Li        """Waits for the landing page screen."""
275*9c5db199SXin Li        self.wait_for_telemetry_commands()
276*9c5db199SXin Li        self._cfmApi.wait_for_meetings_landing_page()
277*9c5db199SXin Li
278*9c5db199SXin Li
279*9c5db199SXin Li    # UI commands/functions
280*9c5db199SXin Li    def wait_for_oobe_start_page(self):
281*9c5db199SXin Li        """Wait for oobe start screen to launch."""
282*9c5db199SXin Li        logging.info('Waiting for OOBE screen')
283*9c5db199SXin Li        self._cfmApi.wait_for_oobe_start_page()
284*9c5db199SXin Li
285*9c5db199SXin Li
286*9c5db199SXin Li    def skip_oobe_screen(self):
287*9c5db199SXin Li        """Skip Chromebox for Meetings oobe screen."""
288*9c5db199SXin Li        logging.info('Skipping OOBE screen')
289*9c5db199SXin Li        self._cfmApi.skip_oobe_screen()
290*9c5db199SXin Li
291*9c5db199SXin Li
292*9c5db199SXin Li    def is_oobe_start_page(self):
293*9c5db199SXin Li        """Check if device is on CFM oobe start screen.
294*9c5db199SXin Li
295*9c5db199SXin Li        @return a boolean, based on oobe start page status.
296*9c5db199SXin Li        """
297*9c5db199SXin Li        return self._cfmApi.is_oobe_start_page()
298*9c5db199SXin Li
299*9c5db199SXin Li
300*9c5db199SXin Li    # Hangouts commands/functions
301*9c5db199SXin Li    def start_new_hangout_session(self, session_name):
302*9c5db199SXin Li        """Start a new hangout session.
303*9c5db199SXin Li
304*9c5db199SXin Li        @param session_name: Name of the hangout session.
305*9c5db199SXin Li        """
306*9c5db199SXin Li        self._cfmApi.start_new_hangout_session(session_name)
307*9c5db199SXin Li
308*9c5db199SXin Li
309*9c5db199SXin Li    def end_hangout_session(self):
310*9c5db199SXin Li        """End current hangout session."""
311*9c5db199SXin Li        self._cfmApi.end_hangout_session()
312*9c5db199SXin Li
313*9c5db199SXin Li
314*9c5db199SXin Li    def is_in_hangout_session(self):
315*9c5db199SXin Li        """Check if device is in hangout session.
316*9c5db199SXin Li
317*9c5db199SXin Li        @return a boolean, for hangout session state.
318*9c5db199SXin Li        """
319*9c5db199SXin Li        return self._cfmApi.is_in_hangout_session()
320*9c5db199SXin Li
321*9c5db199SXin Li
322*9c5db199SXin Li    def is_ready_to_start_hangout_session(self):
323*9c5db199SXin Li        """Check if device is ready to start a new hangout session.
324*9c5db199SXin Li
325*9c5db199SXin Li        @return a boolean for hangout session ready state.
326*9c5db199SXin Li        """
327*9c5db199SXin Li        return self._cfmApi.is_ready_to_start_hangout_session()
328*9c5db199SXin Li
329*9c5db199SXin Li
330*9c5db199SXin Li    def join_meeting_session(self, session_name):
331*9c5db199SXin Li        """Joins a meeting.
332*9c5db199SXin Li
333*9c5db199SXin Li        @param session_name: Name of the meeting session.
334*9c5db199SXin Li        """
335*9c5db199SXin Li        self._cfmApi.join_meeting_session(session_name)
336*9c5db199SXin Li
337*9c5db199SXin Li
338*9c5db199SXin Li    def start_meeting_session(self):
339*9c5db199SXin Li        """Start a meeting.
340*9c5db199SXin Li
341*9c5db199SXin Li        @return code for the started meeting
342*9c5db199SXin Li        """
343*9c5db199SXin Li        return self._cfmApi.start_meeting_session()
344*9c5db199SXin Li
345*9c5db199SXin Li
346*9c5db199SXin Li    def end_meeting_session(self):
347*9c5db199SXin Li        """End current meeting session."""
348*9c5db199SXin Li        self._cfmApi.end_meeting_session()
349*9c5db199SXin Li
350*9c5db199SXin Li
351*9c5db199SXin Li    def get_participant_count(self):
352*9c5db199SXin Li        """Gets the total participant count in a call."""
353*9c5db199SXin Li        return self._cfmApi.get_participant_count()
354*9c5db199SXin Li
355*9c5db199SXin Li
356*9c5db199SXin Li    # Diagnostics commands/functions
357*9c5db199SXin Li    def is_diagnostic_run_in_progress(self):
358*9c5db199SXin Li        """Check if hotrod diagnostics is running.
359*9c5db199SXin Li
360*9c5db199SXin Li        @return a boolean for diagnostic run state.
361*9c5db199SXin Li        """
362*9c5db199SXin Li        return self._cfmApi.is_diagnostic_run_in_progress()
363*9c5db199SXin Li
364*9c5db199SXin Li
365*9c5db199SXin Li    def wait_for_diagnostic_run_to_complete(self):
366*9c5db199SXin Li        """Wait for hotrod diagnostics to complete."""
367*9c5db199SXin Li        self._cfmApi.wait_for_diagnostic_run_to_complete()
368*9c5db199SXin Li
369*9c5db199SXin Li
370*9c5db199SXin Li    def run_diagnostics(self):
371*9c5db199SXin Li        """Run hotrod diagnostics."""
372*9c5db199SXin Li        self._cfmApi.run_diagnostics()
373*9c5db199SXin Li
374*9c5db199SXin Li
375*9c5db199SXin Li    def get_last_diagnostics_results(self):
376*9c5db199SXin Li        """Get latest hotrod diagnostics results.
377*9c5db199SXin Li
378*9c5db199SXin Li        @return a dict with diagnostic test results.
379*9c5db199SXin Li        """
380*9c5db199SXin Li        return self._cfmApi.get_last_diagnostics_results()
381*9c5db199SXin Li
382*9c5db199SXin Li
383*9c5db199SXin Li    # Mic audio commands/functions
384*9c5db199SXin Li    def is_mic_muted(self):
385*9c5db199SXin Li        """Check if mic is muted.
386*9c5db199SXin Li
387*9c5db199SXin Li        @return a boolean for mic mute state.
388*9c5db199SXin Li        """
389*9c5db199SXin Li        return self._cfmApi.is_mic_muted()
390*9c5db199SXin Li
391*9c5db199SXin Li
392*9c5db199SXin Li    def mute_mic(self):
393*9c5db199SXin Li        """Local mic mute from toolbar."""
394*9c5db199SXin Li        self._cfmApi.mute_mic()
395*9c5db199SXin Li
396*9c5db199SXin Li
397*9c5db199SXin Li    def unmute_mic(self):
398*9c5db199SXin Li        """Local mic unmute from toolbar."""
399*9c5db199SXin Li        self._cfmApi.unmute_mic()
400*9c5db199SXin Li
401*9c5db199SXin Li
402*9c5db199SXin Li    def remote_mute_mic(self):
403*9c5db199SXin Li        """Remote mic mute request from cPanel."""
404*9c5db199SXin Li        self._cfmApi.remote_mute_mic()
405*9c5db199SXin Li
406*9c5db199SXin Li
407*9c5db199SXin Li    def remote_unmute_mic(self):
408*9c5db199SXin Li        """Remote mic unmute request from cPanel."""
409*9c5db199SXin Li        self._cfmApi.remote_unmute_mic()
410*9c5db199SXin Li
411*9c5db199SXin Li
412*9c5db199SXin Li    def get_mic_devices(self):
413*9c5db199SXin Li        """Get all mic devices detected by hotrod.
414*9c5db199SXin Li
415*9c5db199SXin Li        @return a list of mic devices.
416*9c5db199SXin Li        """
417*9c5db199SXin Li        return self._cfmApi.get_mic_devices()
418*9c5db199SXin Li
419*9c5db199SXin Li
420*9c5db199SXin Li    def get_preferred_mic(self):
421*9c5db199SXin Li        """Get mic preferred for hotrod.
422*9c5db199SXin Li
423*9c5db199SXin Li        @return a str with preferred mic name.
424*9c5db199SXin Li        """
425*9c5db199SXin Li        return self._cfmApi.get_preferred_mic()
426*9c5db199SXin Li
427*9c5db199SXin Li
428*9c5db199SXin Li    def set_preferred_mic(self, mic):
429*9c5db199SXin Li        """Set preferred mic for hotrod.
430*9c5db199SXin Li
431*9c5db199SXin Li        @param mic: String with mic name.
432*9c5db199SXin Li        """
433*9c5db199SXin Li        self._cfmApi.set_preferred_mic(mic)
434*9c5db199SXin Li
435*9c5db199SXin Li
436*9c5db199SXin Li    # Speaker commands/functions
437*9c5db199SXin Li    def get_speaker_devices(self):
438*9c5db199SXin Li        """Get all speaker devices detected by hotrod.
439*9c5db199SXin Li
440*9c5db199SXin Li        @return a list of speaker devices.
441*9c5db199SXin Li        """
442*9c5db199SXin Li        return self._cfmApi.get_speaker_devices()
443*9c5db199SXin Li
444*9c5db199SXin Li
445*9c5db199SXin Li    def get_preferred_speaker(self):
446*9c5db199SXin Li        """Get speaker preferred for hotrod.
447*9c5db199SXin Li
448*9c5db199SXin Li        @return a str with preferred speaker name.
449*9c5db199SXin Li        """
450*9c5db199SXin Li        return self._cfmApi.get_preferred_speaker()
451*9c5db199SXin Li
452*9c5db199SXin Li
453*9c5db199SXin Li    def set_preferred_speaker(self, speaker):
454*9c5db199SXin Li        """Set preferred speaker for hotrod.
455*9c5db199SXin Li
456*9c5db199SXin Li        @param speaker: String with speaker name.
457*9c5db199SXin Li        """
458*9c5db199SXin Li        self._cfmApi.set_preferred_speaker(speaker)
459*9c5db199SXin Li
460*9c5db199SXin Li
461*9c5db199SXin Li    def set_speaker_volume(self, volume_level):
462*9c5db199SXin Li        """Set speaker volume.
463*9c5db199SXin Li
464*9c5db199SXin Li        @param volume_level: String value ranging from 0-100 to set volume to.
465*9c5db199SXin Li        """
466*9c5db199SXin Li        self._cfmApi.set_speaker_volume(volume_level)
467*9c5db199SXin Li
468*9c5db199SXin Li
469*9c5db199SXin Li    def get_speaker_volume(self):
470*9c5db199SXin Li        """Get current speaker volume.
471*9c5db199SXin Li
472*9c5db199SXin Li        @return a str value with speaker volume level 0-100.
473*9c5db199SXin Li        """
474*9c5db199SXin Li        return self._cfmApi.get_speaker_volume()
475*9c5db199SXin Li
476*9c5db199SXin Li
477*9c5db199SXin Li    def play_test_sound(self):
478*9c5db199SXin Li        """Play test sound."""
479*9c5db199SXin Li        self._cfmApi.play_test_sound()
480*9c5db199SXin Li
481*9c5db199SXin Li
482*9c5db199SXin Li    # Camera commands/functions
483*9c5db199SXin Li    def get_camera_devices(self):
484*9c5db199SXin Li        """Get all camera devices detected by hotrod.
485*9c5db199SXin Li
486*9c5db199SXin Li        @return a list of camera devices.
487*9c5db199SXin Li        """
488*9c5db199SXin Li        return self._cfmApi.get_camera_devices()
489*9c5db199SXin Li
490*9c5db199SXin Li
491*9c5db199SXin Li    def get_preferred_camera(self):
492*9c5db199SXin Li        """Get camera preferred for hotrod.
493*9c5db199SXin Li
494*9c5db199SXin Li        @return a str with preferred camera name.
495*9c5db199SXin Li        """
496*9c5db199SXin Li        return self._cfmApi.get_preferred_camera()
497*9c5db199SXin Li
498*9c5db199SXin Li
499*9c5db199SXin Li    def set_preferred_camera(self, camera):
500*9c5db199SXin Li        """Set preferred camera for hotrod.
501*9c5db199SXin Li
502*9c5db199SXin Li        @param camera: String with camera name.
503*9c5db199SXin Li        """
504*9c5db199SXin Li        self._cfmApi.set_preferred_camera(camera)
505*9c5db199SXin Li
506*9c5db199SXin Li
507*9c5db199SXin Li    def is_camera_muted(self):
508*9c5db199SXin Li        """Check if camera is muted (turned off).
509*9c5db199SXin Li
510*9c5db199SXin Li        @return a boolean for camera muted state.
511*9c5db199SXin Li        """
512*9c5db199SXin Li        return self._cfmApi.is_camera_muted()
513*9c5db199SXin Li
514*9c5db199SXin Li
515*9c5db199SXin Li    def mute_camera(self):
516*9c5db199SXin Li        """Turned camera off."""
517*9c5db199SXin Li        self._cfmApi.mute_camera()
518*9c5db199SXin Li
519*9c5db199SXin Li
520*9c5db199SXin Li    def unmute_camera(self):
521*9c5db199SXin Li        """Turned camera on."""
522*9c5db199SXin Li        self._cfmApi.unmute_camera()
523*9c5db199SXin Li
524*9c5db199SXin Li    def move_camera(self, camera_motion):
525*9c5db199SXin Li        """Move camera(PTZ commands).
526*9c5db199SXin Li
527*9c5db199SXin Li        @param camera_motion: Set of allowed commands
528*9c5db199SXin Li            defined in cfmApi.move_camera.
529*9c5db199SXin Li        """
530*9c5db199SXin Li        self._cfmApi.move_camera(camera_motion)
531*9c5db199SXin Li
532*9c5db199SXin Li    def _convert_large_integers(self, o):
533*9c5db199SXin Li        if type(o) is list:
534*9c5db199SXin Li            return [self._convert_large_integers(x) for x in o]
535*9c5db199SXin Li        elif type(o) is dict:
536*9c5db199SXin Li            return {
537*9c5db199SXin Li                    k: self._convert_large_integers(v)
538*9c5db199SXin Li                    for k, v in six.iteritems(o)
539*9c5db199SXin Li            }
540*9c5db199SXin Li        else:
541*9c5db199SXin Li            if type(o) is int and o > six.moves.xmlrpc_client.MAXINT:
542*9c5db199SXin Li                return float(o)
543*9c5db199SXin Li            else:
544*9c5db199SXin Li                return o
545*9c5db199SXin Li
546*9c5db199SXin Li    def get_media_info_data_points(self):
547*9c5db199SXin Li        """
548*9c5db199SXin Li        Gets media info data points containing media stats.
549*9c5db199SXin Li
550*9c5db199SXin Li        These are exported on the window object when the
551*9c5db199SXin Li        ExportMediaInfo mod is enabled.
552*9c5db199SXin Li
553*9c5db199SXin Li        @returns A list with dictionaries of media info data points.
554*9c5db199SXin Li        @raises RuntimeError if the data point API is not available.
555*9c5db199SXin Li        """
556*9c5db199SXin Li        is_api_available_script = (
557*9c5db199SXin Li                '"realtime" in window '
558*9c5db199SXin Li                '&& "media" in realtime '
559*9c5db199SXin Li                '&& "getMediaInfoDataPoints" in realtime.media')
560*9c5db199SXin Li        if not self._webview_context.EvaluateJavaScript(
561*9c5db199SXin Li                is_api_available_script):
562*9c5db199SXin Li            raise RuntimeError(
563*9c5db199SXin Li                    'realtime.media.getMediaInfoDataPoints not available. '
564*9c5db199SXin Li                    'Is the ExportMediaInfo mod active? '
565*9c5db199SXin Li                    'The mod is only available for Meet.')
566*9c5db199SXin Li
567*9c5db199SXin Li        # Sanitize the timestamp on the JS side to work around crbug.com/851482.
568*9c5db199SXin Li        # Use JSON stringify/parse to create a deep copy of the data point.
569*9c5db199SXin Li        get_data_points_js_script = """
570*9c5db199SXin Li            var dataPoints = window.realtime.media.getMediaInfoDataPoints();
571*9c5db199SXin Li            dataPoints.map((point) => {
572*9c5db199SXin Li                var sanitizedPoint = JSON.parse(JSON.stringify(point));
573*9c5db199SXin Li                sanitizedPoint["timestamp"] /= 1000.0;
574*9c5db199SXin Li                return sanitizedPoint;
575*9c5db199SXin Li            });"""
576*9c5db199SXin Li
577*9c5db199SXin Li        data_points = self._webview_context.EvaluateJavaScript(
578*9c5db199SXin Li            get_data_points_js_script)
579*9c5db199SXin Li        # XML RCP gives overflow errors when trying to send too large
580*9c5db199SXin Li        # integers or longs so we convert media stats to floats.
581*9c5db199SXin Li        data_points = self._convert_large_integers(data_points)
582*9c5db199SXin Li        return data_points
583