1# Copyright 2024 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"""Utility functions for interacting with a device via the UI.""" 15 16import datetime 17import logging 18import re 19import time 20import types 21import xml.etree.ElementTree as et 22 23import camera_properties_utils 24import its_device_utils 25 26_DIR_EXISTS_TXT = 'Directory exists' 27_PERMISSIONS_LIST = ('CAMERA', 'RECORD_AUDIO', 'ACCESS_FINE_LOCATION', 28 'ACCESS_COARSE_LOCATION') 29 30ACTION_ITS_DO_JCA_CAPTURE = ( 31 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_CAPTURE' 32) 33ACTION_ITS_DO_JCA_VIDEO_CAPTURE = ( 34 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_VIDEO_CAPTURE' 35) 36ACTIVITY_WAIT_TIME_SECONDS = 5 37AGREE_BUTTON = 'Agree' 38AGREE_AND_CONTINUE_BUTTON = 'Agree and continue' 39CANCEL_BUTTON_TXT = 'Cancel' 40CAMERA_FILES_PATHS = ('/sdcard/DCIM/Camera', 41 '/storage/emulated/0/Pictures') 42CAPTURE_BUTTON_RESOURCE_ID = 'CaptureButton' 43DONE_BUTTON_TXT = 'Done' 44FLASH_MODE_TO_CLICKS = types.MappingProxyType({ 45 'OFF': 3, 46 'AUTO': 2 47}) 48IMG_CAPTURE_CMD = 'am start -a android.media.action.IMAGE_CAPTURE' 49ITS_ACTIVITY_TEXT = 'Camera ITS Test' 50JPG_FORMAT_STR = '.jpg' 51OK_BUTTON_TXT = 'OK' 52TAKE_PHOTO_CMD = 'input keyevent KEYCODE_CAMERA' 53QUICK_SETTINGS_RESOURCE_ID = 'QuickSettingsDropDown' 54QUICK_SET_FLASH_RESOURCE_ID = 'QuickSettingsFlashButton' 55QUICK_SET_FLIP_CAMERA_RESOURCE_ID = 'QuickSettingsFlipCameraButton' 56QUICK_SET_RATIO_RESOURCE_ID = 'QuickSettingsRatioButton' 57RATIO_TO_UI_DESCRIPTION = { 58 '1 to 1 aspect ratio': 'QuickSettingsRatio1:1Button', 59 '3 to 4 aspect ratio': 'QuickSettingsRatio3:4Button', 60 '9 to 16 aspect ratio': 'QuickSettingsRatio9:16Button' 61} 62REMOVE_CAMERA_FILES_CMD = 'rm ' 63UI_DESCRIPTION_BACK_CAMERA = 'Back Camera' 64UI_DESCRIPTION_FRONT_CAMERA = 'Front Camera' 65UI_OBJECT_WAIT_TIME_SECONDS = datetime.timedelta(seconds=3) 66VIEWFINDER_NOT_VISIBLE_PREFIX = 'viewfinder_not_visible' 67VIEWFINDER_VISIBLE_PREFIX = 'viewfinder_visible' 68WAIT_INTERVAL_FIVE_SECONDS = datetime.timedelta(seconds=5) 69 70 71def _find_ui_object_else_click(object_to_await, object_to_click): 72 """Waits for a UI object to be visible. If not, clicks another UI object. 73 74 Args: 75 object_to_await: A snippet-uiautomator selector object to be awaited. 76 object_to_click: A snippet-uiautomator selector object to be clicked. 77 """ 78 if not object_to_await.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 79 object_to_click.click() 80 81 82def verify_ui_object_visible(ui_object, call_on_fail=None): 83 """Verifies that a UI object is visible. 84 85 Args: 86 ui_object: A snippet-uiautomator selector object. 87 call_on_fail: [Optional] Callable; method to call on failure. 88 """ 89 ui_object_visible = ui_object.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS) 90 if not ui_object_visible: 91 if call_on_fail is not None: 92 call_on_fail() 93 raise AssertionError('UI object was not visible!') 94 95 96def open_jca_viewfinder(dut, log_path, request_video_capture=False): 97 """Sends an intent to JCA and open its viewfinder. 98 99 Args: 100 dut: An Android controller device object. 101 log_path: str; Log path to save screenshots. 102 request_video_capture: boolean; True if requesting video capture. 103 Raises: 104 AssertionError: If JCA viewfinder is not visible. 105 """ 106 its_device_utils.start_its_test_activity(dut.serial) 107 call_on_fail = lambda: dut.take_screenshot(log_path, prefix='its_not_found') 108 verify_ui_object_visible( 109 dut.ui(text=ITS_ACTIVITY_TEXT), 110 call_on_fail=call_on_fail 111 ) 112 113 # Send intent to ItsTestActivity, which will start the correct JCA activity. 114 if request_video_capture: 115 its_device_utils.run( 116 f'adb -s {dut.serial} shell am broadcast -a' 117 f'{ACTION_ITS_DO_JCA_VIDEO_CAPTURE}' 118 ) 119 else: 120 its_device_utils.run( 121 f'adb -s {dut.serial} shell am broadcast -a' 122 f'{ACTION_ITS_DO_JCA_CAPTURE}' 123 ) 124 jca_capture_button_visible = dut.ui( 125 res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( 126 UI_OBJECT_WAIT_TIME_SECONDS) 127 if not jca_capture_button_visible: 128 dut.take_screenshot(log_path, prefix=VIEWFINDER_NOT_VISIBLE_PREFIX) 129 logging.debug('Current UI dump: %s', dut.ui.dump()) 130 raise AssertionError('JCA was not started successfully!') 131 dut.take_screenshot(log_path, prefix=VIEWFINDER_VISIBLE_PREFIX) 132 133 134def switch_jca_camera(dut, log_path, facing): 135 """Interacts with JCA UI to switch camera if necessary. 136 137 Args: 138 dut: An Android controller device object. 139 log_path: str; log path to save screenshots. 140 facing: str; constant describing the direction the camera lens faces. 141 Raises: 142 AssertionError: If JCA does not report that camera has been switched. 143 """ 144 if facing == camera_properties_utils.LENS_FACING['BACK']: 145 ui_facing_description = UI_DESCRIPTION_BACK_CAMERA 146 elif facing == camera_properties_utils.LENS_FACING['FRONT']: 147 ui_facing_description = UI_DESCRIPTION_FRONT_CAMERA 148 else: 149 raise ValueError(f'Unknown facing: {facing}') 150 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 151 _find_ui_object_else_click(dut.ui(desc=ui_facing_description), 152 dut.ui(res=QUICK_SET_FLIP_CAMERA_RESOURCE_ID)) 153 if not dut.ui(desc=ui_facing_description).wait.exists( 154 UI_OBJECT_WAIT_TIME_SECONDS): 155 dut.take_screenshot(log_path, prefix='failed_to_switch_camera') 156 logging.debug('JCA UI dump: %s', dut.ui.dump()) 157 raise AssertionError(f'Failed to switch to {ui_facing_description}!') 158 dut.take_screenshot( 159 log_path, prefix=f"switched_to_{ui_facing_description.replace(' ', '_')}" 160 ) 161 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 162 163 164def change_jca_aspect_ratio(dut, log_path, aspect_ratio): 165 """Interacts with JCA UI to change aspect ratio if necessary. 166 167 Args: 168 dut: An Android controller device object. 169 log_path: str; log path to save screenshots. 170 aspect_ratio: str; Aspect ratio that JCA supports. 171 Acceptable values: _RATIO_TO_UI_DESCRIPTION 172 Raises: 173 ValueError: If ratio is not supported in JCA. 174 AssertionError: If JCA does not find the requested ratio. 175 """ 176 if aspect_ratio not in RATIO_TO_UI_DESCRIPTION: 177 raise ValueError(f'Testing ratio {aspect_ratio} not supported in JCA!') 178 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 179 # Change aspect ratio in ratio switching menu if needed 180 if not dut.ui(desc=aspect_ratio).wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 181 dut.ui(res=QUICK_SET_RATIO_RESOURCE_ID).click() 182 try: 183 dut.ui(res=RATIO_TO_UI_DESCRIPTION[aspect_ratio]).click() 184 except Exception as e: 185 dut.take_screenshot( 186 log_path, prefix=f'failed_to_find{aspect_ratio.replace(" ", "_")}' 187 ) 188 raise AssertionError( 189 f'Testing ratio {aspect_ratio} not found in JCA app UI!') from e 190 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 191 192 193def do_jca_video_setup(dut, log_path, facing, aspect_ratio): 194 """Change video capture settings using the UI. 195 196 Selects UI elements to modify settings. 197 198 Args: 199 dut: An Android controller device object. 200 log_path: str; log path to save screenshots. 201 facing: str; constant describing the direction the camera lens faces. 202 Acceptable values: camera_properties_utils.LENS_FACING[BACK, FRONT] 203 aspect_ratio: str; Aspect ratios that JCA supports. 204 Acceptable values: _RATIO_TO_UI_DESCRIPTION 205 """ 206 open_jca_viewfinder(dut, log_path, request_video_capture=True) 207 switch_jca_camera(dut, log_path, facing) 208 change_jca_aspect_ratio(dut, log_path, aspect_ratio) 209 210 211def default_camera_app_setup(device_id, pkg_name): 212 """Setup Camera app by providing required permissions. 213 214 Args: 215 device_id: serial id of device. 216 pkg_name: pkg name of the app to setup. 217 Returns: 218 Runtime exception from called function or None. 219 """ 220 logging.debug('Setting up the app with permission.') 221 for permission in _PERMISSIONS_LIST: 222 cmd = f'pm grant {pkg_name} android.permission.{permission}' 223 its_device_utils.run_adb_shell_command(device_id, cmd) 224 allow_manage_storage_cmd = ( 225 f'appops set {pkg_name} MANAGE_EXTERNAL_STORAGE allow' 226 ) 227 its_device_utils.run_adb_shell_command(device_id, allow_manage_storage_cmd) 228 229 230def switch_default_camera(dut, facing, log_path): 231 """Interacts with default camera app UI to switch camera. 232 233 Args: 234 dut: An Android controller device object. 235 facing: str; constant describing the direction the camera lens faces. 236 log_path: str; log path to save screenshots. 237 Raises: 238 AssertionError: If default camera app does not report that 239 camera has been switched. 240 """ 241 flip_camera_pattern = ( 242 r'(switch to|flip camera|switch camera|camera switch|switch)' 243 ) 244 default_ui_dump = dut.ui.dump() 245 logging.debug('Default camera UI dump: %s', default_ui_dump) 246 root = et.fromstring(default_ui_dump) 247 camera_flip_res = False 248 for node in root.iter('node'): 249 resource_id = node.get('resource-id') 250 content_desc = node.get('content-desc') 251 if re.search( 252 flip_camera_pattern, content_desc, re.IGNORECASE 253 ): 254 logging.debug('Pattern matches') 255 logging.debug('Resource id: %s', resource_id) 256 logging.debug('Flip camera content-desc: %s', content_desc) 257 camera_flip_res = True 258 break 259 if content_desc and resource_id: 260 if facing == 'front' and camera_flip_res: 261 if ('rear' in content_desc.lower() or 'rear' in resource_id.lower() 262 or 'back' in content_desc.lower() or 'back' in resource_id.lower() 263 ): 264 logging.debug('Pattern found but camera is already switched.') 265 else: 266 dut.ui(desc=content_desc).click.wait() 267 elif facing == 'rear' and camera_flip_res: 268 if 'front' in content_desc.lower() or 'front' in resource_id.lower(): 269 logging.debug('Pattern found but camera is already switched.') 270 else: 271 dut.ui(desc=content_desc).click.wait() 272 else: 273 raise ValueError(f'Unknown facing: {facing}') 274 275 dut.take_screenshot( 276 log_path, prefix=f'switched_to_{facing}_default_camera' 277 ) 278 279 if not camera_flip_res: 280 raise AssertionError('Flip camera resource not found.') 281 282 283def pull_img_files(device_id, input_path, output_path): 284 """Pulls files from the input_path on the device to output_path. 285 286 Args: 287 device_id: serial id of device. 288 input_path: File location on device. 289 output_path: Location to save the file on the host. 290 """ 291 logging.debug('Pulling files from the device') 292 pull_cmd = f'adb -s {device_id} pull {input_path} {output_path}' 293 its_device_utils.run(pull_cmd) 294 295 296def launch_and_take_capture(dut, pkg_name, camera_facing, log_path): 297 """Launches the camera app and takes still capture. 298 299 Args: 300 dut: An Android controller device object. 301 pkg_name: pkg_name of the default camera app to 302 be used for captures. 303 camera_facing: camera lens facing orientation 304 log_path: str; log path to save screenshots. 305 306 Returns: 307 img_path_on_dut: Path of the captured image on the device 308 """ 309 device_id = dut.serial 310 try: 311 logging.debug('Launching app: %s', pkg_name) 312 launch_cmd = f'monkey -p {pkg_name} 1' 313 its_device_utils.run_adb_shell_command(device_id, launch_cmd) 314 315 # Click OK/Done button on initial pop up windows 316 if dut.ui(text=AGREE_BUTTON).wait.exists( 317 timeout=WAIT_INTERVAL_FIVE_SECONDS): 318 dut.ui(text=AGREE_BUTTON).click.wait() 319 if dut.ui(text=AGREE_AND_CONTINUE_BUTTON).wait.exists( 320 timeout=WAIT_INTERVAL_FIVE_SECONDS): 321 dut.ui(text=AGREE_AND_CONTINUE_BUTTON).click.wait() 322 if dut.ui(text=OK_BUTTON_TXT).wait.exists( 323 timeout=WAIT_INTERVAL_FIVE_SECONDS): 324 dut.ui(text=OK_BUTTON_TXT).click.wait() 325 if dut.ui(text=DONE_BUTTON_TXT).wait.exists( 326 timeout=WAIT_INTERVAL_FIVE_SECONDS): 327 dut.ui(text=DONE_BUTTON_TXT).click.wait() 328 if dut.ui(text=CANCEL_BUTTON_TXT).wait.exists( 329 timeout=WAIT_INTERVAL_FIVE_SECONDS): 330 dut.ui(text=CANCEL_BUTTON_TXT).click.wait() 331 switch_default_camera(dut, camera_facing, log_path) 332 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 333 logging.debug('Taking photo') 334 its_device_utils.run_adb_shell_command(device_id, TAKE_PHOTO_CMD) 335 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 336 img_path_on_dut = '' 337 photo_storage_path = '' 338 for path in CAMERA_FILES_PATHS: 339 check_path_cmd = ( 340 f'ls {path} && echo "Directory exists" || ' 341 'echo "Directory does not exist"' 342 ) 343 cmd_output = dut.adb.shell(check_path_cmd).decode('utf-8').strip() 344 if _DIR_EXISTS_TXT in cmd_output: 345 photo_storage_path = path 346 break 347 find_file_path = ( 348 f'find {photo_storage_path} ! -empty -a ! -name \'.pending*\'' 349 ' -a -type f -iname "*.jpg" -o -iname "*.jpeg"' 350 ) 351 img_path_on_dut = ( 352 dut.adb.shell(find_file_path).decode('utf-8').strip().lower() 353 ) 354 logging.debug('Image path on DUT: %s', img_path_on_dut) 355 if JPG_FORMAT_STR not in img_path_on_dut: 356 raise AssertionError('Failed to find jpg files!') 357 finally: 358 force_stop_app(dut, pkg_name) 359 return img_path_on_dut 360 361 362def force_stop_app(dut, pkg_name): 363 """Force stops an app with given pkg_name. 364 365 Args: 366 dut: An Android controller device object. 367 pkg_name: pkg_name of the app to be stopped. 368 """ 369 logging.debug('Closing app: %s', pkg_name) 370 force_stop_cmd = f'am force-stop {pkg_name}' 371 dut.adb.shell(force_stop_cmd) 372 373 374def default_camera_app_dut_setup(device_id, pkg_name): 375 """Setup the device for testing default camera app. 376 377 Args: 378 device_id: serial id of device. 379 pkg_name: pkg_name of the app. 380 Returns: 381 Runtime exception from called function or None. 382 """ 383 default_camera_app_setup(device_id, pkg_name) 384 for path in CAMERA_FILES_PATHS: 385 its_device_utils.run_adb_shell_command( 386 device_id, f'{REMOVE_CAMERA_FILES_CMD}{path}/*') 387