1# Copyright 2023 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import ast
16import logging
17import os
18import platform
19import subprocess
20import time
21
22from mobly import asserts
23from mobly import base_test
24from mobly import test_runner
25from mobly.controllers import android_device
26
27
28class DeviceAsWebcamTest(base_test.BaseTestClass):
29  # Tests device as webcam functionality with Mobly base test class to run.
30
31  _ACTION_WEBCAM_RESULT = 'com.android.cts.verifier.camera.webcam.ACTION_WEBCAM_RESULT'
32  _WEBCAM_RESULTS = 'camera.webcam.extra.RESULTS'
33  _WEBCAM_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.webcam.WebcamTestActivity'
34  _DAC_PREVIEW_ACTIVITY = 'com.android.DeviceAsWebcam/.DeviceAsWebcamPreview'
35  _ACTIVITY_START_WAIT = 1.5  # seconds
36  _ADB_RESTART_WAIT = 9  # seconds
37  _FPS_TOLERANCE = 0.15 # 15 percent
38  _RESULT_PASS = 'PASS'
39  _RESULT_FAIL = 'FAIL'
40  _RESULT_NOT_EXECUTED = 'NOT_EXECUTED'
41  _MANUAL_FRAME_CHECK_DURATION = 8  # seconds
42  _WINDOWS_OS = 'Windows'
43  _MAC_OS = 'Darwin'
44  _LINUX_OS = 'Linux'
45
46  def run_os_specific_test(self):
47    """Runs the os specific webcam test script.
48
49    Returns:
50      A result list of tuples (tested_fps, actual_fps)
51    """
52    results = []
53    current_os = platform.system()
54
55    if current_os == self._WINDOWS_OS:
56      import windows_webcam_test
57      logging.info('Starting test on Windows')
58      # Due to compatibility issues directly running the windows
59      # main function, the results from the windows_webcam_test script
60      # are printed to the stdout and retrieved
61      output = subprocess.check_output(['python', 'windows_webcam_test.py'])
62      output_str = output.decode('utf-8')
63      results = ast.literal_eval(output_str.strip())
64    elif current_os == self._LINUX_OS:
65      import linux_webcam_test
66      logging.info('Starting test on Linux')
67      results = linux_webcam_test.main()
68    elif current_os == self._MAC_OS:
69      import mac_webcam_test
70      logging.info('Starting test on Mac')
71      results = mac_webcam_test.main()
72    else:
73      logging.info('Running on an unknown OS')
74
75    return results
76
77  def validate_fps(self, results):
78    """Verifies the webcam FPS falls within the acceptable range of the tested FPS.
79
80    Args:
81        results: A result list of tuples (tested_fps, actual_fps)
82
83    Returns:
84        True if all FPS are within tolerance range, False otherwise
85    """
86    result = True
87
88    for elem in results:
89      tested_fps = elem[0]
90      actual_fps = elem[1]
91
92      max_diff = tested_fps * self._FPS_TOLERANCE
93
94      if abs(tested_fps - actual_fps) > max_diff:
95        logging.error('FPS is out of tolerance range! '
96                      ' Tested: %d Actual FPS: %d', tested_fps, actual_fps)
97        result = False
98
99    return result
100
101  def run_cmd(self, cmd):
102    """Replaces os.system call, while hiding stdout+stderr messages."""
103    with open(os.devnull, 'wb') as devnull:
104      subprocess.check_call(cmd.split(), stdout=devnull,
105                            stderr=subprocess.STDOUT)
106
107  def setup_class(self):
108    # Registering android_device controller module declares the test
109    # dependencies on Android device hardware. By default, we expect at least
110    # one object is created from this.
111    devices = self.register_controller(android_device, min_number=1)
112    self.dut = devices[0]
113    self.dut.adb.root()
114
115  def test_webcam(self):
116
117    adb = f'adb -s {self.dut.serial}'
118
119    # Keep device on while testing since it requires a manual check on the
120    # webcam frames
121    # '7' is a combination of flags ORed together to keep the device on
122    # in all cases
123    self.dut.adb.shell(['settings', 'put', 'global',
124                        'stay_on_while_plugged_in', '7'])
125
126    cmd = f"""{adb} shell am start {self._WEBCAM_TEST_ACTIVITY}
127        --activity-brought-to-front"""
128    self.run_cmd(cmd)
129
130    # Check if webcam feature is enabled
131    dut_webcam_enabled = self.dut.adb.shell(['getprop', 'ro.usb.uvc.enabled'])
132    if 'true' in dut_webcam_enabled.decode('utf-8'):
133      logging.info('Webcam enabled, testing webcam')
134    else:
135      logging.info('Webcam not enabled, skipping webcam test')
136
137      # Notify CTSVerifier test that the webcam test was skipped,
138      # the test will be marked as PASSED for this case
139      cmd = (f"""{adb} shell am broadcast -a
140          {self._ACTION_WEBCAM_RESULT} --es {self._WEBCAM_RESULTS}
141          {self._RESULT_NOT_EXECUTED}""")
142      self.run_cmd(cmd)
143
144      return
145
146    # Set USB preference option to webcam
147    set_uvc = self.dut.adb.shell(['svc', 'usb', 'setFunctions', 'uvc'])
148    if not set_uvc:
149      logging.error('USB preference option to set webcam unsuccessful')
150
151      # Notify CTSVerifier test that setting webcam option was unsuccessful
152      cmd = (f"""{adb} shell am broadcast -a
153          {self._ACTION_WEBCAM_RESULT} --es {self._WEBCAM_RESULTS}
154          {self._RESULT_FAIL}""")
155      self.run_cmd(cmd)
156      return
157
158    # After resetting the USB preference, adb disconnects
159    # and reconnects so wait for device
160    time.sleep(self._ADB_RESTART_WAIT)
161
162    fps_results = self.run_os_specific_test()
163    logging.info('FPS test results (Expected, Actual): %s', fps_results)
164    result = self.validate_fps(fps_results)
165
166    test_status = self._RESULT_PASS
167    if not result or not fps_results:
168      logging.info('FPS testing failed')
169      test_status = self._RESULT_FAIL
170
171    # Send result to CTSVerifier test
172    time.sleep(self._ACTIVITY_START_WAIT)
173    cmd = (f"""{adb} shell am broadcast -a
174        {self._ACTION_WEBCAM_RESULT} --es {self._WEBCAM_RESULTS}
175        {test_status}""")
176    self.run_cmd(cmd)
177
178    # Enable the webcam service preview activity for a manual
179    # check on webcam frames
180    cmd = f"""{adb} shell am start {self._DAC_PREVIEW_ACTIVITY}
181        --activity-no-history"""
182    self.run_cmd(cmd)
183    time.sleep(self._MANUAL_FRAME_CHECK_DURATION)
184
185    cmd = f"""{adb} shell am start {self._WEBCAM_TEST_ACTIVITY}
186        --activity-brought-to-front"""
187    self.run_cmd(cmd)
188
189    asserts.assert_true(test_status == self._RESULT_PASS, 'Results: Failed')
190
191    self.dut.adb.shell(['settings', 'put',
192                        'global', 'stay_on_while_plugged_in', '0'])
193
194if __name__ == '__main__':
195  test_runner.main()
196