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 time 19import types 20 21import camera_properties_utils 22import its_device_utils 23 24_DIR_EXISTS_TXT = 'Directory exists' 25_PERMISSIONS_LIST = ('CAMERA', 'RECORD_AUDIO', 'ACCESS_FINE_LOCATION', 26 'ACCESS_COARSE_LOCATION') 27 28ACTION_ITS_DO_JCA_CAPTURE = ( 29 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_CAPTURE' 30) 31ACTIVITY_WAIT_TIME_SECONDS = 5 32AGREE_BUTTON = 'Agree and continue' 33CAMERA_FILES_PATHS = ('/sdcard/DCIM/Camera', 34 '/storage/emulated/0/Pictures') 35CAPTURE_BUTTON_RESOURCE_ID = 'CaptureButton' 36DONE_BUTTON_TXT = 'Done' 37FLASH_MODE_TO_CLICKS = types.MappingProxyType({ 38 'OFF': 3, 39 'AUTO': 2 40}) 41IMG_CAPTURE_CMD = 'am start -a android.media.action.IMAGE_CAPTURE' 42ITS_ACTIVITY_TEXT = 'Camera ITS Test' 43JPG_FORMAT_STR = '.jpg' 44OK_BUTTON_TXT = 'OK' 45TAKE_PHOTO_CMD = 'input keyevent KEYCODE_CAMERA' 46QUICK_SETTINGS_RESOURCE_ID = 'QuickSettingsDropDown' 47QUICK_SET_FLASH_RESOURCE_ID = 'QuickSettingsFlashButton' 48QUICK_SET_FLIP_CAMERA_RESOURCE_ID = 'QuickSettingsFlipCameraButton' 49REMOVE_CAMERA_FILES_CMD = 'rm ' 50UI_DESCRIPTION_BACK_CAMERA = 'Back Camera' 51UI_DESCRIPTION_FRONT_CAMERA = 'Front Camera' 52UI_OBJECT_WAIT_TIME_SECONDS = datetime.timedelta(seconds=3) 53VIEWFINDER_NOT_VISIBLE_PREFIX = 'viewfinder_not_visible' 54VIEWFINDER_VISIBLE_PREFIX = 'viewfinder_visible' 55WAIT_INTERVAL_FIVE_SECONDS = datetime.timedelta(seconds=5) 56 57 58def _find_ui_object_else_click(object_to_await, object_to_click): 59 """Waits for a UI object to be visible. If not, clicks another UI object. 60 61 Args: 62 object_to_await: A snippet-uiautomator selector object to be awaited. 63 object_to_click: A snippet-uiautomator selector object to be clicked. 64 """ 65 if not object_to_await.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 66 object_to_click.click() 67 68 69def verify_ui_object_visible(ui_object, call_on_fail=None): 70 """Verifies that a UI object is visible. 71 72 Args: 73 ui_object: A snippet-uiautomator selector object. 74 call_on_fail: [Optional] Callable; method to call on failure. 75 """ 76 ui_object_visible = ui_object.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS) 77 if not ui_object_visible: 78 if call_on_fail is not None: 79 call_on_fail() 80 raise AssertionError('UI object was not visible!') 81 82 83def open_jca_viewfinder(dut, log_path): 84 """Sends an intent to JCA and open its viewfinder. 85 86 Args: 87 dut: An Android controller device object. 88 log_path: str; log path to save screenshots. 89 Raises: 90 AssertionError: If JCA viewfinder is not visible. 91 """ 92 its_device_utils.start_its_test_activity(dut.serial) 93 call_on_fail = lambda: dut.take_screenshot(log_path, prefix='its_not_found') 94 verify_ui_object_visible( 95 dut.ui(text=ITS_ACTIVITY_TEXT), 96 call_on_fail=call_on_fail 97 ) 98 99 # Send intent to ItsTestActivity, which will start the correct JCA activity. 100 its_device_utils.run( 101 f'adb -s {dut.serial} shell am broadcast -a {ACTION_ITS_DO_JCA_CAPTURE}' 102 ) 103 jca_capture_button_visible = dut.ui( 104 res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( 105 UI_OBJECT_WAIT_TIME_SECONDS) 106 if not jca_capture_button_visible: 107 dut.take_screenshot(log_path, prefix=VIEWFINDER_NOT_VISIBLE_PREFIX) 108 logging.debug('Current UI dump: %s', dut.ui.dump()) 109 raise AssertionError('JCA was not started successfully!') 110 dut.take_screenshot(log_path, prefix=VIEWFINDER_VISIBLE_PREFIX) 111 112 113def switch_jca_camera(dut, log_path, facing): 114 """Interacts with JCA UI to switch camera if necessary. 115 116 Args: 117 dut: An Android controller device object. 118 log_path: str; log path to save screenshots. 119 facing: str; constant describing the direction the camera lens faces. 120 Raises: 121 AssertionError: If JCA does not report that camera has been switched. 122 """ 123 if facing == camera_properties_utils.LENS_FACING['BACK']: 124 ui_facing_description = UI_DESCRIPTION_BACK_CAMERA 125 elif facing == camera_properties_utils.LENS_FACING['FRONT']: 126 ui_facing_description = UI_DESCRIPTION_FRONT_CAMERA 127 else: 128 raise ValueError(f'Unknown facing: {facing}') 129 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 130 _find_ui_object_else_click(dut.ui(desc=ui_facing_description), 131 dut.ui(res=QUICK_SET_FLIP_CAMERA_RESOURCE_ID)) 132 if not dut.ui(desc=ui_facing_description).wait.exists( 133 UI_OBJECT_WAIT_TIME_SECONDS): 134 dut.take_screenshot(log_path, prefix='failed_to_switch_camera') 135 logging.debug('JCA UI dump: %s', dut.ui.dump()) 136 raise AssertionError(f'Failed to switch to {ui_facing_description}!') 137 dut.take_screenshot( 138 log_path, prefix=f"switched_to_{ui_facing_description.replace(' ', '_')}" 139 ) 140 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 141 142 143def default_camera_app_setup(device_id, pkg_name): 144 """Setup Camera app by providing required permissions. 145 146 Args: 147 device_id: serial id of device. 148 pkg_name: pkg name of the app to setup. 149 Returns: 150 Runtime exception from called function or None. 151 """ 152 logging.debug('Setting up the app with permission.') 153 for permission in _PERMISSIONS_LIST: 154 cmd = f'pm grant {pkg_name} android.permission.{permission}' 155 its_device_utils.run_adb_shell_command(device_id, cmd) 156 allow_manage_storage_cmd = ( 157 f'appops set {pkg_name} MANAGE_EXTERNAL_STORAGE allow' 158 ) 159 its_device_utils.run_adb_shell_command(device_id, allow_manage_storage_cmd) 160 161 162def pull_img_files(device_id, input_path, output_path): 163 """Pulls files from the input_path on the device to output_path. 164 165 Args: 166 device_id: serial id of device. 167 input_path: File location on device. 168 output_path: Location to save the file on the host. 169 """ 170 logging.debug('Pulling files from the device') 171 pull_cmd = f'adb -s {device_id} pull {input_path} {output_path}' 172 its_device_utils.run(pull_cmd) 173 174 175def launch_and_take_capture(dut, pkg_name): 176 """Launches the camera app and takes still capture. 177 178 Args: 179 dut: An Android controller device object. 180 pkg_name: pkg_name of the default camera app to 181 be used for captures. 182 183 Returns: 184 img_path_on_dut: Path of the captured image on the device 185 """ 186 device_id = dut.serial 187 try: 188 logging.debug('Launching app: %s', pkg_name) 189 launch_cmd = f'monkey -p {pkg_name} 1' 190 its_device_utils.run_adb_shell_command(device_id, launch_cmd) 191 192 # Click OK/Done button on initial pop up windows 193 if dut.ui(text=AGREE_BUTTON).wait.exists( 194 timeout=WAIT_INTERVAL_FIVE_SECONDS): 195 dut.ui(text=AGREE_BUTTON).click.wait() 196 if dut.ui(text=OK_BUTTON_TXT).wait.exists( 197 timeout=WAIT_INTERVAL_FIVE_SECONDS): 198 dut.ui(text=OK_BUTTON_TXT).click.wait() 199 if dut.ui(text=DONE_BUTTON_TXT).wait.exists( 200 timeout=WAIT_INTERVAL_FIVE_SECONDS): 201 dut.ui(text=DONE_BUTTON_TXT).click.wait() 202 203 logging.debug('Taking photo') 204 its_device_utils.run_adb_shell_command(device_id, TAKE_PHOTO_CMD) 205 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 206 img_path_on_dut = '' 207 photo_storage_path = '' 208 for path in CAMERA_FILES_PATHS: 209 check_path_cmd = ( 210 f'ls {path} && echo "Directory exists" || ' 211 'echo "Directory does not exist"' 212 ) 213 cmd_output = dut.adb.shell(check_path_cmd).decode('utf-8').strip() 214 if _DIR_EXISTS_TXT in cmd_output: 215 photo_storage_path = path 216 break 217 find_file_path = ( 218 f'find {photo_storage_path} ! -empty -a ! -name \'.pending*\'' 219 ' -a -type f -name "*.jpg" -o -name "*.jpeg"' 220 ) 221 img_path_on_dut = dut.adb.shell(find_file_path).decode('utf-8').strip() 222 logging.debug('Image path on DUT: %s', img_path_on_dut) 223 if JPG_FORMAT_STR not in img_path_on_dut: 224 raise AssertionError('Failed to find jpg files!') 225 finally: 226 force_stop_app(dut, pkg_name) 227 return img_path_on_dut 228 229 230def force_stop_app(dut, pkg_name): 231 """Force stops an app with given pkg_name. 232 233 Args: 234 dut: An Android controller device object. 235 pkg_name: pkg_name of the app to be stopped. 236 """ 237 logging.debug('Closing app: %s', pkg_name) 238 force_stop_cmd = f'am force-stop {pkg_name}' 239 dut.adb.shell(force_stop_cmd) 240 241 242def default_camera_app_dut_setup(device_id, pkg_name): 243 """Setup the device for testing default camera app. 244 245 Args: 246 device_id: serial id of device. 247 pkg_name: pkg_name of the app. 248 Returns: 249 Runtime exception from called function or None. 250 """ 251 default_camera_app_setup(device_id, pkg_name) 252 for path in CAMERA_FILES_PATHS: 253 its_device_utils.run_adb_shell_command( 254 device_id, f'{REMOVE_CAMERA_FILES_CMD}{path}/*') 255