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 dataclasses 17import datetime 18import logging 19import math 20import os 21import re 22import subprocess 23import time 24import xml.etree.ElementTree as et 25 26import camera_properties_utils 27import error_util 28import its_device_utils 29 30 31_DIR_EXISTS_TXT = 'Directory exists' 32_PERMISSIONS_LIST = ('CAMERA', 'RECORD_AUDIO', 'ACCESS_FINE_LOCATION', 33 'ACCESS_COARSE_LOCATION') 34 35ACTION_ITS_DO_JCA_CAPTURE = ( 36 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_CAPTURE' 37) 38ACTION_ITS_DO_JCA_VIDEO_CAPTURE = ( 39 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_VIDEO_CAPTURE' 40) 41ACTIVITY_WAIT_TIME_SECONDS = 5 42AGREE_BUTTON = 'Agree' 43AGREE_AND_CONTINUE_BUTTON = 'Agree and continue' 44CANCEL_BUTTON_TXT = 'Cancel' 45CAMERA_FILES_PATHS = ('/sdcard/DCIM/Camera', 46 '/storage/emulated/0/Pictures', 47 '/sdcard/DCIM',) 48CAPTURE_BUTTON_RESOURCE_ID = 'CaptureButton' 49DEFAULT_CAMERA_APP_DUMPSYS_PATH = '/sdcard/default_camera_dumpsys.txt' 50DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR = ',' 51DEFAULT_JCA_UI_DUMPSYS_PATH = '/sdcard/jca-ui-dumpsys.txt' 52DONE_BUTTON_TXT = 'Done' 53EMULATED_STORAGE_PATH = '/storage/emulated/0/Pictures' 54 55# TODO: b/383392277 - use resource IDs instead of content descriptions. 56FLASH_MODE_ON_CONTENT_DESC = 'Flash on' 57FLASH_MODE_OFF_CONTENT_DESC = 'Flash off' 58FLASH_MODE_AUTO_CONTENT_DESC = 'Auto flash' 59FLASH_MODE_LOW_LIGHT_BOOST_CONTENT_DESC = 'Low Light Boost on' 60FLASH_MODES = ( 61 FLASH_MODE_ON_CONTENT_DESC, 62 FLASH_MODE_OFF_CONTENT_DESC, 63 FLASH_MODE_AUTO_CONTENT_DESC, 64 FLASH_MODE_LOW_LIGHT_BOOST_CONTENT_DESC 65) 66IMG_CAPTURE_CMD = 'am start -a android.media.action.IMAGE_CAPTURE' 67ITS_ACTIVITY_TEXT = 'Camera ITS Test' 68JETPACK_CAMERA_APP_PACKAGE_NAME = 'com.google.jetpackcamera' 69JPG_FORMAT_STR = '.jpg' 70LOCATION_ON_TXT = 'Turn on' 71OK_BUTTON_TXT = 'OK' 72TAKE_PHOTO_CMD = 'input keyevent KEYCODE_CAMERA' 73QUICK_SETTINGS_RESOURCE_ID = 'QuickSettingsDropDown' 74QUICK_SET_FLASH_RESOURCE_ID = 'QuickSettingsFlashButton' 75QUICK_SET_FLIP_CAMERA_RESOURCE_ID = 'QuickSettingsFlipCameraButton' 76QUICK_SET_RATIO_RESOURCE_ID = 'QuickSettingsRatioButton' 77RATIO_TO_UI_DESCRIPTION = { 78 '1 to 1 aspect ratio': 'QuickSettingsRatio1:1Button', 79 '3 to 4 aspect ratio': 'QuickSettingsRatio3:4Button', 80 '9 to 16 aspect ratio': 'QuickSettingsRatio9:16Button' 81} 82REMOVE_CAMERA_FILES_CMD = 'rm -rf' 83SETTINGS_BACK_BUTTON_RESOURCE_ID = 'BackButton' 84SETTINGS_BUTTON_RESOURCE_ID = 'SettingsButton' 85SETTINGS_CLOSE_TEXT = 'Close' 86SETTINGS_VIDEO_STABILIZATION_AUTO_TEXT = 'Stabilization Auto' 87SETTINGS_MENU_STABILIZATION_HIGH_QUALITY_TEXT = 'Stabilization High Quality' 88SETTINGS_VIDEO_STABILIZATION_MODE_TEXT = 'Set Video Stabilization' 89SETTINGS_MENU_STABILIZATION_OFF_TEXT = 'Stabilization Off' 90THREE_TO_FOUR_ASPECT_RATIO_DESC = '3 to 4 aspect ratio' 91UI_DESCRIPTION_BACK_CAMERA = 'Back Camera' 92UI_DESCRIPTION_FRONT_CAMERA = 'Front Camera' 93UI_OBJECT_WAIT_TIME_SECONDS = datetime.timedelta(seconds=3) 94UI_PHYSICAL_CAMERA_RESOURCE_ID = 'PhysicalCameraIdTag' 95UI_ZOOM_RATIO_TEXT_RESOURCE_ID = 'ZoomRatioTag' 96UI_DEBUG_OVERLAY_BUTTON_RESOURCE_ID = 'DebugOverlayButton' 97UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_BUTTON_RESOURCE_ID = ( 98 'DebugOverlaySetZoomRatioButton' 99) 100UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD_RESOURCE_ID = ( 101 'DebugOverlaySetZoomRatioTextField' 102) 103UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_SET_BUTTON_RESOURCE_ID = ( 104 'DebugOverlaySetZoomRatioSetButton' 105) 106UI_IMAGE_CAPTURE_SUCCESS_TEXT = 'Image Capture Success' 107VIEWFINDER_NOT_VISIBLE_PREFIX = 'viewfinder_not_visible' 108VIEWFINDER_VISIBLE_PREFIX = 'viewfinder_visible' 109WAIT_INTERVAL_FIVE_SECONDS = datetime.timedelta(seconds=5) 110JCA_WATCH_DUMP_FILE = 'jca_watch_dump.txt' 111DEFAULT_CAMERA_WATCH_DUMP_FILE = 'default_camera_watch_dump.txt' 112WATCH_WAIT_TIME_SECONDS = 2 113_CONTROL_ZOOM_RATIO_KEY = 'android.control.zoomRatio' 114_REQ_STR_PATTERN = 'REQ' 115JCA_VIDEO_STABILIZATION_MODE_OFF = 0 116JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY = 1 117JCA_VIDEO_STABILIZATION_MODE_ON = 2 118JCA_VIDEO_STABILIZATION_MODE_OPTICAL = 3 119JCA_STABILIZATION_MODES = { 120 0: 'Off', 121 1: 'High Quality', 122 2: 'On', 123 3: 'Optical' 124} 125 126 127@dataclasses.dataclass(frozen=True) 128class JcaCapture: 129 capture_path: str 130 physical_id: int 131 132 133def _find_ui_object_else_click(object_to_await, object_to_click): 134 """Waits for a UI object to be visible. If not, clicks another UI object. 135 136 Args: 137 object_to_await: A snippet-uiautomator selector object to be awaited. 138 object_to_click: A snippet-uiautomator selector object to be clicked. 139 """ 140 if not object_to_await.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 141 object_to_click.click() 142 143 144def verify_ui_object_visible(ui_object, call_on_fail=None): 145 """Verifies that a UI object is visible. 146 147 Args: 148 ui_object: A snippet-uiautomator selector object. 149 call_on_fail: [Optional] Callable; method to call on failure. 150 """ 151 ui_object_visible = ui_object.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS) 152 if not ui_object_visible: 153 if call_on_fail is not None: 154 call_on_fail() 155 raise AssertionError('UI object was not visible!') 156 157 158def open_jca_viewfinder(dut, log_path, request_video_capture=False): 159 """Sends an intent to JCA and open its viewfinder. 160 161 Args: 162 dut: An Android controller device object. 163 log_path: str; Log path to save screenshots. 164 request_video_capture: boolean; True if requesting video capture. 165 Raises: 166 AssertionError: If JCA viewfinder is not visible. 167 """ 168 its_device_utils.start_its_test_activity(dut.serial) 169 call_on_fail = lambda: dut.take_screenshot(log_path, prefix='its_not_found') 170 verify_ui_object_visible( 171 dut.ui(text=ITS_ACTIVITY_TEXT), 172 call_on_fail=call_on_fail 173 ) 174 175 # Send intent to ItsTestActivity, which will start the correct JCA activity. 176 if request_video_capture: 177 its_device_utils.run( 178 f'adb -s {dut.serial} shell am broadcast -a' 179 f'{ACTION_ITS_DO_JCA_VIDEO_CAPTURE}' 180 ) 181 else: 182 its_device_utils.run( 183 f'adb -s {dut.serial} shell am broadcast -a' 184 f'{ACTION_ITS_DO_JCA_CAPTURE}' 185 ) 186 jca_capture_button_visible = dut.ui( 187 res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( 188 UI_OBJECT_WAIT_TIME_SECONDS) 189 if not jca_capture_button_visible: 190 dut.take_screenshot(log_path, prefix=VIEWFINDER_NOT_VISIBLE_PREFIX) 191 logging.debug('Current UI dump: %s', dut.ui.dump()) 192 raise AssertionError('JCA was not started successfully!') 193 dut.take_screenshot(log_path, prefix=VIEWFINDER_VISIBLE_PREFIX) 194 195 196def switch_jca_camera(dut, log_path, facing): 197 """Interacts with JCA UI to switch camera if necessary. 198 199 Args: 200 dut: An Android controller device object. 201 log_path: str; log path to save screenshots. 202 facing: str; constant describing the direction the camera lens faces. 203 Raises: 204 AssertionError: If JCA does not report that camera has been switched. 205 """ 206 if facing == camera_properties_utils.LENS_FACING['BACK']: 207 ui_facing_description = UI_DESCRIPTION_BACK_CAMERA 208 elif facing == camera_properties_utils.LENS_FACING['FRONT']: 209 ui_facing_description = UI_DESCRIPTION_FRONT_CAMERA 210 else: 211 raise ValueError(f'Unknown facing: {facing}') 212 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 213 _find_ui_object_else_click(dut.ui(desc=ui_facing_description), 214 dut.ui(res=QUICK_SET_FLIP_CAMERA_RESOURCE_ID)) 215 if not dut.ui(desc=ui_facing_description).wait.exists( 216 UI_OBJECT_WAIT_TIME_SECONDS): 217 dut.take_screenshot(log_path, prefix='failed_to_switch_camera') 218 logging.debug('JCA UI dump: %s', dut.ui.dump()) 219 raise AssertionError(f'Failed to switch to {ui_facing_description}!') 220 dut.take_screenshot( 221 log_path, prefix=f"switched_to_{ui_facing_description.replace(' ', '_')}" 222 ) 223 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 224 225 226def _get_current_flash_mode_desc(dut): 227 """Returns the current flash mode description from the JCA UI.""" 228 dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).wait.exists( 229 UI_OBJECT_WAIT_TIME_SECONDS) 230 return dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).child(depth=1).description 231 232 233def set_jca_flash_mode(dut, log_path, flash_mode_desc): 234 """Interacts with JCA UI to set flash mode if necessary. 235 236 Args: 237 dut: An Android controller device object. 238 log_path: str; log path to save screenshots. 239 flash_mode_desc: str; flash mode description to set. 240 Acceptable values: FLASH_MODES 241 Raises: 242 AssertionError: If JCA fails to set the desired flash mode. 243 """ 244 if flash_mode_desc not in FLASH_MODES: 245 raise ValueError( 246 f'Invalid flash mode description: {flash_mode_desc}. ' 247 f'Valid values: {FLASH_MODES}' 248 ) 249 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 250 current_flash_mode_desc = _get_current_flash_mode_desc(dut) 251 initial_flash_mode_desc = current_flash_mode_desc 252 logging.debug('Initial flash mode description: %s', initial_flash_mode_desc) 253 if initial_flash_mode_desc == flash_mode_desc: 254 logging.debug('Initial flash mode %s matches desired flash mode %s', 255 initial_flash_mode_desc, flash_mode_desc) 256 else: 257 while current_flash_mode_desc != flash_mode_desc: 258 dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).click() 259 current_flash_mode_desc = _get_current_flash_mode_desc(dut) 260 if current_flash_mode_desc == initial_flash_mode_desc: 261 raise AssertionError(f'Failed to set flash mode to {flash_mode_desc}!') 262 if not dut.ui(desc=flash_mode_desc).wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 263 logging.debug('JCA UI dump: %s', dut.ui.dump()) 264 dut.take_screenshot(log_path, prefix='cannot_set_flash_mode') 265 raise AssertionError(f'Unable to confirm {flash_mode_desc} exists in UI') 266 dut.take_screenshot(log_path, prefix='flash_mode_set') 267 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 268 269 270def jca_ui_zoom(dut, zoom_ratio, log_path): 271 """Interacts with the debug JCA overlay UI to zoom to the desired zoom ratio. 272 273 Args: 274 dut: An Android controller device object. 275 zoom_ratio: float; zoom ratio desired. Will be rounded for compatibility. 276 log_path: str; log path to save screenshots. 277 Raises: 278 AssertionError: If desired zoom ratio cannot be reached. 279 """ 280 zoom_ratio = round(zoom_ratio, 2) # JCA only supports 2 decimal places 281 current_zoom_ratio_text = dut.ui(res=UI_ZOOM_RATIO_TEXT_RESOURCE_ID).text 282 logging.debug('current zoom ratio text: %s', current_zoom_ratio_text) 283 current_zoom_ratio = float(current_zoom_ratio_text[:-1]) # remove `x` 284 if math.isclose(zoom_ratio, current_zoom_ratio): 285 logging.debug('Desired zoom ratio is %.2f, ' 286 'current zoom ratio is %.2f. ' 287 'No need to zoom.', 288 zoom_ratio, current_zoom_ratio) 289 return 290 dut.ui(res=UI_DEBUG_OVERLAY_BUTTON_RESOURCE_ID).click() 291 dut.ui(res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_BUTTON_RESOURCE_ID).click() 292 dut.ui( 293 res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD_RESOURCE_ID 294 ).set_text(str(zoom_ratio)) 295 dut.ui(res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_SET_BUTTON_RESOURCE_ID).click() 296 # Ensure that preview is stable by clicking the center of the screen. 297 center_x, center_y = ( 298 dut.ui.info['displayWidth'] // 2, 299 dut.ui.info['displayHeight'] // 2 300 ) 301 dut.ui.click(x=center_x, y=center_y) 302 time.sleep(UI_OBJECT_WAIT_TIME_SECONDS.total_seconds()) 303 zoom_ratio_text_after_zoom = dut.ui(res=UI_ZOOM_RATIO_TEXT_RESOURCE_ID).text 304 logging.debug('zoom ratio text after zoom: %s', zoom_ratio_text_after_zoom) 305 zoom_ratio_after_zoom = float(zoom_ratio_text_after_zoom[:-1]) # remove `x` 306 if not math.isclose(zoom_ratio, zoom_ratio_after_zoom): 307 dut.take_screenshot( 308 log_path, prefix=f'failed_to_zoom_to_{zoom_ratio}' 309 ) 310 raise AssertionError( 311 f'Failed to zoom to {zoom_ratio}, ' 312 f'zoomed to {zoom_ratio_after_zoom} instead.' 313 ) 314 logging.debug('Set zoom ratio to %.2f', zoom_ratio) 315 dut.take_screenshot(log_path, prefix=f'zoomed_to_{zoom_ratio}') 316 317 318def change_jca_aspect_ratio(dut, log_path, aspect_ratio): 319 """Interacts with JCA UI to change aspect ratio if necessary. 320 321 Args: 322 dut: An Android controller device object. 323 log_path: str; log path to save screenshots. 324 aspect_ratio: str; Aspect ratio that JCA supports. 325 Acceptable values: _RATIO_TO_UI_DESCRIPTION 326 Raises: 327 ValueError: If ratio is not supported in JCA. 328 AssertionError: If JCA does not find the requested ratio. 329 """ 330 if aspect_ratio not in RATIO_TO_UI_DESCRIPTION: 331 raise ValueError(f'Testing ratio {aspect_ratio} not supported in JCA!') 332 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 333 # Change aspect ratio in ratio switching menu if needed 334 if not dut.ui(desc=aspect_ratio).wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): 335 dut.ui(res=QUICK_SET_RATIO_RESOURCE_ID).click() 336 try: 337 dut.ui(res=RATIO_TO_UI_DESCRIPTION[aspect_ratio]).click() 338 except Exception as e: 339 dut.take_screenshot( 340 log_path, prefix=f'failed_to_find{aspect_ratio.replace(" ", "_")}' 341 ) 342 raise AssertionError( 343 f'Testing ratio {aspect_ratio} not found in JCA app UI!') from e 344 dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() 345 346 347def do_jca_video_setup(dut, log_path, facing, aspect_ratio, stabilization_mode): 348 """Change video capture settings using the UI. 349 350 Selects UI elements to modify settings. 351 352 Args: 353 dut: An Android controller device object. 354 log_path: str; log path to save screenshots. 355 facing: str; constant describing the direction the camera lens faces. 356 Acceptable values: camera_properties_utils.LENS_FACING[BACK, FRONT] 357 aspect_ratio: str; Aspect ratios that JCA supports. 358 Acceptable values: _RATIO_TO_UI_DESCRIPTION 359 stabilization_mode: int; constant describing the video stabilization mode. 360 Acceptable values: 0, 1, 2 361 """ 362 open_jca_viewfinder(dut, log_path, request_video_capture=True) 363 switch_jca_camera(dut, log_path, facing) 364 change_jca_aspect_ratio(dut, log_path, aspect_ratio) 365 _set_jca_video_stabilization(dut, log_path, stabilization_mode) 366 367 368def _set_jca_video_stabilization(dut, log_path, stabilization_mode): 369 """Change video stabilization mode using the UI. 370 371 Args: 372 dut: An Android controller device object. 373 log_path: str; log path to save screenshots. 374 stabilization_mode: int; constant describing the video stabilization mode. 375 Acceptable values: JCA_VIDEO_STABILIZATION_MODE_OFF, 376 JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY, 377 JCA_VIDEO_STABILIZATION_MODE_ON 378 JCA_VIDEO_STABILIZATION_MODE_OPTICAL 379 Mapping of JCA modes: 380 ON: corresponds to setting android.control.videoStabilizationMode 381 to PREVIEW_STABILIZATION. 382 HIGH_QUALITY: corresponds to setting android.control.videoStabilizationMode 383 to ON 384 AUTO: will set the stabilization mode to PREVIEW_STABILIZATION, 385 if the lens supports it, and if not, it will set it to OIS. 386 If neither preview stabilization or OIS are supported it will be OFF. 387 OPTICAL: optical stabilization is turned on in the default camera app 388 when the video stabilization mode is OFF 389 """ 390 dut.ui(res=SETTINGS_BUTTON_RESOURCE_ID).click() 391 if not dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).wait.exists( 392 UI_OBJECT_WAIT_TIME_SECONDS): 393 dut.take_screenshot( 394 log_path, prefix='failed_to_find_video_stabilization_settings') 395 raise AssertionError( 396 'Set Video Stabilization settings not found!' 397 'Make sure you have the latest JCA app.' 398 ) 399 dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).click() 400 401 if not dut.ui(text=JCA_STABILIZATION_MODES[stabilization_mode]).wait.exists( 402 UI_OBJECT_WAIT_TIME_SECONDS): 403 dut.take_screenshot( 404 log_path, prefix='failed_to_find_video_stabilization_mode') 405 raise AssertionError( 406 'Video Stabilization Mode not found!' 407 ) 408 409 # Ensure that the stabilzation options are enabled. 410 # They will be disabled if the camera does not support stabilization 411 if not dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).enabled: 412 raise AssertionError('Set Video Stabilization not enabled.') 413 414 dut.ui(text=JCA_STABILIZATION_MODES[stabilization_mode]).click() 415 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 416 logging.debug('JCA Video Stabilization set to %s successfully.', 417 JCA_STABILIZATION_MODES[stabilization_mode]) 418 screenshot_prefix = ( 419 f'jca_stabilization_mode_{JCA_STABILIZATION_MODES[stabilization_mode]}_set' 420 ) 421 dut.take_screenshot(log_path, prefix=screenshot_prefix) 422 dut.ui(text=SETTINGS_CLOSE_TEXT).click() 423 dut.ui(res=SETTINGS_BACK_BUTTON_RESOURCE_ID).click() 424 # Verify that the setting was applied 425 if stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_ON: 426 if not dut.ui(desc='Preview is Stabilized').wait.exists( 427 UI_OBJECT_WAIT_TIME_SECONDS): 428 raise AssertionError('JCA video stabilization_mode not set to ON.') 429 elif stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY: 430 if not dut.ui(desc='Only Video is Stabilized').wait.exists( 431 UI_OBJECT_WAIT_TIME_SECONDS): 432 raise AssertionError( 433 'JCA video stabilization_mode not set to HIGH_QUALITY.') 434 elif stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_OPTICAL: 435 if not dut.ui(desc='Optical stabilization is Enabled').wait.exists( 436 UI_OBJECT_WAIT_TIME_SECONDS): 437 raise AssertionError( 438 'JCA video stabilization_mode not set to OPTICAL.') 439 else: 440 if 'stabilize' in dut.ui.dump().lower(): 441 raise AssertionError('JCA video stabilization_mode not set to OFF.') 442 443 444def default_camera_app_setup(device_id, pkg_name): 445 """Setup Camera app by providing required permissions. 446 447 Args: 448 device_id: serial id of device. 449 pkg_name: pkg name of the app to setup. 450 Returns: 451 Runtime exception from called function or None. 452 """ 453 logging.debug('Setting up the app with permission.') 454 for permission in _PERMISSIONS_LIST: 455 cmd = f'pm grant {pkg_name} android.permission.{permission}' 456 its_device_utils.run_adb_shell_command(device_id, cmd) 457 allow_manage_storage_cmd = ( 458 f'appops set {pkg_name} MANAGE_EXTERNAL_STORAGE allow' 459 ) 460 its_device_utils.run_adb_shell_command(device_id, allow_manage_storage_cmd) 461 462 463def _get_current_camera_facing(content_desc, resource_id): 464 """Returns the current camera facing based on UI elements.""" 465 # If separator is present, the last element is the current camera facing. 466 if DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR in content_desc: 467 current_facing = content_desc.split( 468 DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR)[-1] 469 if 'rear' in current_facing.lower() or 'back' in current_facing.lower(): 470 return 'rear' 471 elif 'front' in current_facing.lower(): 472 return 'front' 473 474 # If separator is not present, the element describes the other camera facing. 475 if ('rear' in content_desc.lower() or 'rear' in resource_id.lower() 476 or 'back' in content_desc.lower() or 'back' in resource_id.lower()): 477 return 'front' 478 elif 'front' in content_desc.lower() or 'front' in resource_id.lower(): 479 return 'rear' 480 else: 481 raise ValueError('Failed to determine current camera facing.') 482 483 484def switch_default_camera(dut, facing, log_path): 485 """Interacts with default camera app UI to switch camera. 486 487 Args: 488 dut: An Android controller device object. 489 facing: str; constant describing the direction the camera lens faces. 490 log_path: str; log path to save screenshots. 491 Raises: 492 AssertionError: If default camera app does not report that 493 camera has been switched. 494 """ 495 flip_camera_pattern = ( 496 r'(switch to|flip camera|switch camera|camera switch|' 497 r'toggle_button|front_back_switcher|switch_camera_button|camera_switch_button)' 498 ) 499 flash_pattern = 'flash' 500 default_ui_dump = dut.ui.dump() 501 logging.debug('Default camera UI dump: %s', default_ui_dump) 502 root = et.fromstring(default_ui_dump) 503 for node in root.iter('node'): 504 resource_id = node.get('resource-id') 505 content_desc = node.get('content-desc') 506 # Ignore resource ids for flash on/off 507 if (re.search(flash_pattern, content_desc, re.IGNORECASE) or 508 re.search(flash_pattern, resource_id, re.IGNORECASE)): 509 continue 510 if content_desc: 511 if re.search( 512 flip_camera_pattern, content_desc, re.IGNORECASE 513 ): 514 logging.debug('Pattern matches') 515 logging.debug('Resource id: %s', resource_id) 516 logging.debug('Flip camera content-desc: %s', content_desc) 517 break 518 else: 519 if re.search( 520 flip_camera_pattern, resource_id, re.IGNORECASE 521 ): 522 logging.debug('Pattern matches') 523 logging.debug('Resource id: %s', resource_id) 524 logging.debug('Flip camera content-desc: %s', content_desc) 525 break 526 else: 527 raise AssertionError('Flip camera resource not found.') 528 if facing == _get_current_camera_facing(content_desc, resource_id): 529 logging.debug('Pattern found but camera is already switched.') 530 else: 531 if content_desc: 532 dut.ui(desc=content_desc).click.wait() 533 else: 534 dut.ui(res=resource_id).click.wait() 535 536 dut.take_screenshot( 537 log_path, prefix=f'switched_to_{facing}_default_camera' 538 ) 539 540 541def pull_img_files(device_id, input_path, output_path): 542 """Pulls files from the input_path on the device to output_path. 543 544 Args: 545 device_id: serial id of device. 546 input_path: File location on device. 547 output_path: Location to save the file on the host. 548 """ 549 logging.debug('Pulling files from the device') 550 pull_cmd = f'adb -s {device_id} pull {input_path} {output_path}' 551 its_device_utils.run(pull_cmd) 552 553 554def launch_and_take_capture(dut, pkg_name, camera_facing, log_path, 555 dumpsys_path=DEFAULT_CAMERA_APP_DUMPSYS_PATH): 556 """Launches the camera app and takes still capture. 557 558 Args: 559 dut: An Android controller device object. 560 pkg_name: pkg_name of the default camera app to 561 be used for captures. 562 camera_facing: camera lens facing orientation 563 log_path: str; log path to save screenshots. 564 dumpsys_path: path of the file on device to store the report 565 566 Returns: 567 img_path_on_dut: Path of the captured image on the device 568 """ 569 device_id = dut.serial 570 # start cameraservice watch command to monitor default camera pkg 571 watch_dump_path = os.path.join(log_path, DEFAULT_CAMERA_WATCH_DUMP_FILE) 572 watch_process = start_cameraservice_watch(device_id, watch_dump_path, 573 pkg_name) 574 try: 575 logging.debug('Launching app: %s', pkg_name) 576 launch_cmd = f'monkey -p {pkg_name} 1' 577 its_device_utils.run_adb_shell_command(device_id, launch_cmd) 578 579 # Click OK/Done button on initial pop up windows 580 if dut.ui(text=AGREE_BUTTON).wait.exists( 581 timeout=WAIT_INTERVAL_FIVE_SECONDS): 582 dut.ui(text=AGREE_BUTTON).click.wait() 583 if dut.ui(text=AGREE_AND_CONTINUE_BUTTON).wait.exists( 584 timeout=WAIT_INTERVAL_FIVE_SECONDS): 585 dut.ui(text=AGREE_AND_CONTINUE_BUTTON).click.wait() 586 if dut.ui(text=OK_BUTTON_TXT).wait.exists( 587 timeout=WAIT_INTERVAL_FIVE_SECONDS): 588 dut.ui(text=OK_BUTTON_TXT).click.wait() 589 if dut.ui(text=DONE_BUTTON_TXT).wait.exists( 590 timeout=WAIT_INTERVAL_FIVE_SECONDS): 591 dut.ui(text=DONE_BUTTON_TXT).click.wait() 592 if dut.ui(text=CANCEL_BUTTON_TXT).wait.exists( 593 timeout=WAIT_INTERVAL_FIVE_SECONDS): 594 dut.ui(text=CANCEL_BUTTON_TXT).click.wait() 595 if dut.ui(text=LOCATION_ON_TXT).wait.exists( 596 timeout=WAIT_INTERVAL_FIVE_SECONDS 597 ): 598 dut.ui(text=LOCATION_ON_TXT).click.wait() 599 switch_default_camera(dut, camera_facing, log_path) 600 take_dumpsys_report(dut, dumpsys_path) 601 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 602 logging.debug('Taking photo') 603 its_device_utils.run_adb_shell_command(device_id, TAKE_PHOTO_CMD) 604 605 # pull the dumpsys output 606 dut.adb.pull([dumpsys_path, log_path]) 607 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 608 # stop cameraservice watch immediately after capturing image 609 stop_cameraservice_watch(watch_process) 610 img_path_on_dut = '' 611 photo_storage_path = '' 612 for path in CAMERA_FILES_PATHS: 613 check_path_cmd = ( 614 f'ls {path} && echo "Directory exists" || ' 615 'echo "Directory does not exist"' 616 ) 617 cmd_output = dut.adb.shell(check_path_cmd).decode('utf-8').strip() 618 if _DIR_EXISTS_TXT in cmd_output: 619 photo_storage_path = path 620 break 621 find_file_path = ( 622 f'find {photo_storage_path} ! -empty -a ! -name \'.pending*\'' 623 ' -a -type f -iname "*.jpg" -o -iname "*.jpeg"' 624 ) 625 img_path_on_dut = ( 626 dut.adb.shell(find_file_path).decode('utf-8').strip().lower() 627 ) 628 logging.debug('Image path on DUT: %s', img_path_on_dut) 629 if JPG_FORMAT_STR not in img_path_on_dut: 630 raise AssertionError('Failed to find jpg files!') 631 finally: 632 force_stop_app(dut, pkg_name) 633 return img_path_on_dut 634 635 636def restart_cts_verifier(dut, package_name): 637 """Sends ADB commands to restart CtsVerifier app.""" 638 # Set correct intent flags so that JCA finishes successfully (b/353830655) 639 force_stop_app(dut, package_name) 640 dut.adb.shell('am start -n com.android.cts.verifier/.CtsVerifierActivity') 641 642 643def force_stop_app(dut, pkg_name): 644 """Force stops an app with given pkg_name. 645 646 Args: 647 dut: An Android controller device object. 648 pkg_name: pkg_name of the app to be stopped. 649 """ 650 logging.debug('Closing app: %s', pkg_name) 651 force_stop_cmd = f'am force-stop {pkg_name}' 652 dut.adb.shell(force_stop_cmd) 653 654 655def default_camera_app_dut_setup(device_id, pkg_name): 656 """Setup the device for testing default camera app. 657 658 Args: 659 device_id: serial id of device. 660 pkg_name: pkg_name of the app. 661 Returns: 662 Runtime exception from called function or None. 663 """ 664 default_camera_app_setup(device_id, pkg_name) 665 for path in CAMERA_FILES_PATHS: 666 its_device_utils.run_adb_shell_command( 667 device_id, f'{REMOVE_CAMERA_FILES_CMD} {path}/*') 668 669 670def launch_jca_and_capture(dut, log_path, camera_facing, zoom_ratio=None, 671 video_stabilization=None): 672 """Launches the jetpack camera app and takes still capture. 673 674 Args: 675 dut: An Android controller device object. 676 log_path: str; log path to save screenshots. 677 camera_facing: camera lens facing orientation 678 zoom_ratio: optional; zoom_ratio to be set while taking the JCA capture. 679 By default it will be set to 1 if the value is None. 680 video_stabilization: optional; video stabilization mode to be set while 681 taking the JCA capture. By default, JCA uses AUTO mode. 682 683 AUTO in JCA will set the stabilization mode to PREVIEW_STABILIZATION, 684 if the lens supports it, and if not, it will set it to OIS. If neither 685 preview stabilization or OIS are supported it will be OFF. 686 687 Returns: 688 img_path_on_dut: Path of the captured image on the device 689 """ 690 device_id = dut.serial 691 remove_command = f'rm -rf {EMULATED_STORAGE_PATH}/*' 692 its_device_utils.run_adb_shell_command(device_id, remove_command) 693 watch_dump_path = os.path.join(log_path, JCA_WATCH_DUMP_FILE) 694 watch_process = start_cameraservice_watch(device_id, watch_dump_path, 695 JETPACK_CAMERA_APP_PACKAGE_NAME) 696 try: 697 logging.debug('Launching JCA app') 698 launch_cmd = ( 699 'am start -n ' 700 f'{JETPACK_CAMERA_APP_PACKAGE_NAME}/{JETPACK_CAMERA_APP_PACKAGE_NAME}.MainActivity ' 701 '--ez "KEY_DEBUG_MODE" true' 702 ) 703 its_device_utils.run_adb_shell_command(device_id, launch_cmd) 704 switch_jca_camera(dut, log_path, camera_facing) 705 change_jca_aspect_ratio(dut, log_path, 706 aspect_ratio=THREE_TO_FOUR_ASPECT_RATIO_DESC) 707 if video_stabilization is not None: 708 _set_jca_video_stabilization(dut, log_path, video_stabilization) 709 # Set zoom_ratio after setting video stabilization to avoid reset to default 710 if zoom_ratio is not None: 711 jca_ui_zoom(dut, zoom_ratio, log_path) 712 # Take dumpsys before capturing the image 713 take_dumpsys_report(dut, file_path=DEFAULT_JCA_UI_DUMPSYS_PATH) 714 if dut.ui(res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( 715 timeout=WAIT_INTERVAL_FIVE_SECONDS 716 ): 717 dut.ui(res=CAPTURE_BUTTON_RESOURCE_ID).click.wait() 718 time.sleep(ACTIVITY_WAIT_TIME_SECONDS) 719 stop_cameraservice_watch(watch_process) 720 # pull the dumpsys output 721 dut.adb.pull([DEFAULT_JCA_UI_DUMPSYS_PATH, log_path]) 722 img_path_on_dut = ( 723 dut.adb.shell( 724 "find {} ! -empty -a ! -name '.pending*' -a -type f".format( 725 EMULATED_STORAGE_PATH 726 ) 727 ) 728 .decode('utf-8') 729 .strip() 730 ) 731 logging.debug('Image path on DUT: %s', img_path_on_dut) 732 if JPG_FORMAT_STR not in img_path_on_dut: 733 raise AssertionError('Failed to find jpg files!') 734 finally: 735 force_stop_app(dut, JETPACK_CAMERA_APP_PACKAGE_NAME) 736 return img_path_on_dut 737 738 739def take_dumpsys_report(dut, file_path): 740 """Takes dumpsys report of camera service and stores the report in the file. 741 742 Args: 743 dut: An Android controller device object. 744 file_path: Path of the file on device to store the report. 745 """ 746 dut.adb.shell(['dumpsys', 'media.camera', '>', file_path]) 747 748 749def _watch_clear(device_id): 750 """Clears cameraservice watch cache. 751 752 Args: 753 device_id: serial id of device. 754 """ 755 cmd = f'adb -s {device_id} shell cmd media.camera watch clear'.split(' ') 756 subprocess.run(cmd, check=True) 757 logging.debug('Cleared watch cache') 758 759 760def _watch_start(device_id, pkg_name): 761 """Starts cameraservice watch command. 762 763 Args: 764 device_id: serial id of device. 765 pkg_name: pkg_name of the app. 766 """ 767 cmd = [ 768 'adb', 769 '-s', 770 device_id, 771 'shell', 772 'cmd', 773 'media.camera', 774 'watch', 775 'start', 776 '-m', 777 ( 778 'android.control.captureIntent,android.jpeg.quality,' 779 'android.control.zoomRatio,' 780 'android.scaler.cropRegion,' 781 'android.control.zoomMethod,' 782 '3a' 783 ), 784 '-c', 785 pkg_name, 786 ] 787 subprocess.run(cmd, check=True) 788 logging.debug('Started watching 3a for %s', pkg_name) 789 790 791def _watch_live(device_id, file_path): 792 """Starts cameraservice watch live command. 793 794 Args: 795 device_id: serial id of device. 796 file_path: Path of the file to store the report. 797 798 Returns: 799 watch_process: subprocess.Popen object for the watch live command. 800 """ 801 cmd = f'adb -s {device_id} shell cmd media.camera watch live'.split(' ') 802 with open(file_path, 'w') as f: 803 logging.debug('Starting watch live') 804 watch_process = subprocess.Popen( 805 cmd, stdout=f, stdin=subprocess.PIPE 806 ) 807 logging.debug('watch live output written to the file_path: %s', file_path) 808 return watch_process 809 810 811def start_cameraservice_watch(device_id, file_path, pkg_name): 812 """Starts cameraservice watch command. 813 814 Args: 815 device_id: serial id of device. 816 file_path: Path of the file to store the report. 817 pkg_name: pkg_name of the app. 818 819 Returns: 820 watch_process: subprocess.Popen object for the watch live command. 821 """ 822 _watch_start(device_id, pkg_name) 823 watch_process = _watch_live(device_id, file_path) 824 watch_process.its_watch_process_device_id = device_id 825 return watch_process 826 827 828def stop_cameraservice_watch(watch_process): 829 """Stops cameraservice watch command. 830 831 Args: 832 watch_process: subprocess.Popen object returned by start_cameraservice_watch 833 Raises: 834 CameraItsError: If watch_process not created by start_cameraservice_watch 835 """ 836 if not hasattr(watch_process, 'its_watch_process_device_id'): 837 raise error_util.CameraItsError( 838 'watch_process was not created by start_cameraservice_watch' 839 ) 840 device_id = watch_process.its_watch_process_device_id 841 watch_process.stdin.write(b'\n') 842 watch_process.stdin.flush() 843 watch_process.wait() 844 logging.debug('Stopping watch live') 845 cmd = f'adb -s {device_id} shell cmd media.camera watch stop'.split(' ') 846 subprocess.run(cmd, check=True) 847 logging.debug('Stopped watching 3a') 848 849 850def get_default_camera_zoom_ratio(file_name): 851 """Returns the zoom_ratio used by default camera capture. 852 853 Args: 854 file_name: str; file name storing default camera pkg watch 855 cameraservice dump output. 856 Returns: 857 zoom_ratio: zoom_ratio rounded up to 2 decimal places 858 Raises: 859 FileNotFoundError: If file_name does not exist 860 """ 861 zoom_ratio_values = [] 862 if not os.path.exists(file_name): 863 raise FileNotFoundError(f'File not found: {file_name}') 864 with open(file_name, 'r') as file: 865 for line in file: 866 if _CONTROL_ZOOM_RATIO_KEY in line: 867 if _REQ_STR_PATTERN not in line: 868 continue 869 logging.debug('zoomRatio line: %s', line) 870 values = line.split(':') 871 value_str = values[-1] 872 match = re.search(r'[\d.]+', value_str) 873 if match: 874 value = float(match.group()) 875 rounded_value = round(value, 2) 876 logging.debug('zoomRatio found: %s', rounded_value) 877 zoom_ratio_values.append(rounded_value) 878 879 if zoom_ratio_values: 880 logging.debug('zoom_ratio_values: %s', zoom_ratio_values) 881 return zoom_ratio_values[-1] 882 return None 883 884 885def get_default_camera_video_stabilization(file_name): 886 """Returns the video stabilization mode used by default camera capture. 887 888 Args: 889 file_name: str; file name storing default camera pkg watch 890 cameraservice dump output. 891 Returns: 892 video_stabilization_mode: str; video stabilization mode used by 893 default camera app during the capture 894 Raises: 895 FileNotFoundError: If file_name does not exist 896 """ 897 video_stabilization_modes = [] 898 if not os.path.exists(file_name): 899 raise FileNotFoundError(f'File not found: {file_name}') 900 with open(file_name, 'r') as file: 901 for line in file: 902 if 'videoStabilizationMode' in line: 903 logging.debug('videoStabilizationMode line: %s', line) 904 values = line.split(':') 905 value_str = values[-1] 906 match = re.search(r'[a-zA-Z]+', value_str) 907 if match: 908 value = str(match.group()) 909 logging.debug('videoStabilizationMode found: %s', value) 910 video_stabilization_modes.append(value) 911 if video_stabilization_modes: 912 logging.debug('video_stabilization_modes: %s', video_stabilization_modes) 913 logging.debug('videoStabilizationMode used for default captures: %s', 914 video_stabilization_modes[-1]) 915 return video_stabilization_modes[-1].strip() 916 return None 917 918 919def get_default_camera_ois_mode(file_name): 920 """Returns the optical stabilization mode used by default camera capture. 921 922 Args: 923 file_name: str; file name storing default camera pkg watch 924 cameraservice dump output. 925 Returns: 926 optical_stabilization_mode: str; optical stabilization mode used by 927 default camera app during the capture 928 Raises: 929 FileNotFoundError: If file_name does not exist 930 """ 931 optical_stabilization_modes = [] 932 if not os.path.exists(file_name): 933 raise FileNotFoundError(f'File not found: {file_name}') 934 with open(file_name, 'r') as file: 935 for line in file: 936 if 'opticalStabilizationMode' in line: 937 if _REQ_STR_PATTERN not in line: 938 continue 939 logging.debug('opticalStabilizationMode line: %s', line) 940 values = line.split(':') 941 value_str = values[-1] 942 match = re.search(r'[a-zA-Z]+', value_str) 943 if match: 944 value = str(match.group()) 945 logging.debug('opticalStabilizationMode found: %s', value) 946 optical_stabilization_modes.append(value) 947 if optical_stabilization_modes: 948 logging.debug('optical_stabilization_modes: %s', 949 optical_stabilization_modes) 950 logging.debug('opticalStabilizationMode used for default captures: %s', 951 optical_stabilization_modes[-1]) 952 return optical_stabilization_modes[-1].strip() 953 return None 954 955