# Copyright 2024 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Utility functions for interacting with a device via the UI.""" import dataclasses import datetime import logging import math import os import re import subprocess import time import xml.etree.ElementTree as et import camera_properties_utils import error_util import its_device_utils _DIR_EXISTS_TXT = 'Directory exists' _PERMISSIONS_LIST = ('CAMERA', 'RECORD_AUDIO', 'ACCESS_FINE_LOCATION', 'ACCESS_COARSE_LOCATION') ACTION_ITS_DO_JCA_CAPTURE = ( 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_CAPTURE' ) ACTION_ITS_DO_JCA_VIDEO_CAPTURE = ( 'com.android.cts.verifier.camera.its.ACTION_ITS_DO_JCA_VIDEO_CAPTURE' ) ACTIVITY_WAIT_TIME_SECONDS = 5 AGREE_BUTTON = 'Agree' AGREE_AND_CONTINUE_BUTTON = 'Agree and continue' CANCEL_BUTTON_TXT = 'Cancel' CAMERA_FILES_PATHS = ('/sdcard/DCIM/Camera', '/storage/emulated/0/Pictures', '/sdcard/DCIM',) CAPTURE_BUTTON_RESOURCE_ID = 'CaptureButton' DEFAULT_CAMERA_APP_DUMPSYS_PATH = '/sdcard/default_camera_dumpsys.txt' DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR = ',' DEFAULT_JCA_UI_DUMPSYS_PATH = '/sdcard/jca-ui-dumpsys.txt' DONE_BUTTON_TXT = 'Done' EMULATED_STORAGE_PATH = '/storage/emulated/0/Pictures' # TODO: b/383392277 - use resource IDs instead of content descriptions. FLASH_MODE_ON_CONTENT_DESC = 'Flash on' FLASH_MODE_OFF_CONTENT_DESC = 'Flash off' FLASH_MODE_AUTO_CONTENT_DESC = 'Auto flash' FLASH_MODE_LOW_LIGHT_BOOST_CONTENT_DESC = 'Low Light Boost on' FLASH_MODES = ( FLASH_MODE_ON_CONTENT_DESC, FLASH_MODE_OFF_CONTENT_DESC, FLASH_MODE_AUTO_CONTENT_DESC, FLASH_MODE_LOW_LIGHT_BOOST_CONTENT_DESC ) IMG_CAPTURE_CMD = 'am start -a android.media.action.IMAGE_CAPTURE' ITS_ACTIVITY_TEXT = 'Camera ITS Test' JETPACK_CAMERA_APP_PACKAGE_NAME = 'com.google.jetpackcamera' JPG_FORMAT_STR = '.jpg' LOCATION_ON_TXT = 'Turn on' OK_BUTTON_TXT = 'OK' TAKE_PHOTO_CMD = 'input keyevent KEYCODE_CAMERA' QUICK_SETTINGS_RESOURCE_ID = 'QuickSettingsDropDown' QUICK_SET_FLASH_RESOURCE_ID = 'QuickSettingsFlashButton' QUICK_SET_FLIP_CAMERA_RESOURCE_ID = 'QuickSettingsFlipCameraButton' QUICK_SET_RATIO_RESOURCE_ID = 'QuickSettingsRatioButton' RATIO_TO_UI_DESCRIPTION = { '1 to 1 aspect ratio': 'QuickSettingsRatio1:1Button', '3 to 4 aspect ratio': 'QuickSettingsRatio3:4Button', '9 to 16 aspect ratio': 'QuickSettingsRatio9:16Button' } REMOVE_CAMERA_FILES_CMD = 'rm -rf' SETTINGS_BACK_BUTTON_RESOURCE_ID = 'BackButton' SETTINGS_BUTTON_RESOURCE_ID = 'SettingsButton' SETTINGS_CLOSE_TEXT = 'Close' SETTINGS_VIDEO_STABILIZATION_AUTO_TEXT = 'Stabilization Auto' SETTINGS_MENU_STABILIZATION_HIGH_QUALITY_TEXT = 'Stabilization High Quality' SETTINGS_VIDEO_STABILIZATION_MODE_TEXT = 'Set Video Stabilization' SETTINGS_MENU_STABILIZATION_OFF_TEXT = 'Stabilization Off' THREE_TO_FOUR_ASPECT_RATIO_DESC = '3 to 4 aspect ratio' UI_DESCRIPTION_BACK_CAMERA = 'Back Camera' UI_DESCRIPTION_FRONT_CAMERA = 'Front Camera' UI_OBJECT_WAIT_TIME_SECONDS = datetime.timedelta(seconds=3) UI_PHYSICAL_CAMERA_RESOURCE_ID = 'PhysicalCameraIdTag' UI_ZOOM_RATIO_TEXT_RESOURCE_ID = 'ZoomRatioTag' UI_DEBUG_OVERLAY_BUTTON_RESOURCE_ID = 'DebugOverlayButton' UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_BUTTON_RESOURCE_ID = ( 'DebugOverlaySetZoomRatioButton' ) UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD_RESOURCE_ID = ( 'DebugOverlaySetZoomRatioTextField' ) UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_SET_BUTTON_RESOURCE_ID = ( 'DebugOverlaySetZoomRatioSetButton' ) UI_IMAGE_CAPTURE_SUCCESS_TEXT = 'Image Capture Success' VIEWFINDER_NOT_VISIBLE_PREFIX = 'viewfinder_not_visible' VIEWFINDER_VISIBLE_PREFIX = 'viewfinder_visible' WAIT_INTERVAL_FIVE_SECONDS = datetime.timedelta(seconds=5) JCA_WATCH_DUMP_FILE = 'jca_watch_dump.txt' DEFAULT_CAMERA_WATCH_DUMP_FILE = 'default_camera_watch_dump.txt' WATCH_WAIT_TIME_SECONDS = 2 _CONTROL_ZOOM_RATIO_KEY = 'android.control.zoomRatio' _REQ_STR_PATTERN = 'REQ' JCA_VIDEO_STABILIZATION_MODE_OFF = 0 JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY = 1 JCA_VIDEO_STABILIZATION_MODE_ON = 2 JCA_VIDEO_STABILIZATION_MODE_OPTICAL = 3 JCA_STABILIZATION_MODES = { 0: 'Off', 1: 'High Quality', 2: 'On', 3: 'Optical' } @dataclasses.dataclass(frozen=True) class JcaCapture: capture_path: str physical_id: int def _find_ui_object_else_click(object_to_await, object_to_click): """Waits for a UI object to be visible. If not, clicks another UI object. Args: object_to_await: A snippet-uiautomator selector object to be awaited. object_to_click: A snippet-uiautomator selector object to be clicked. """ if not object_to_await.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): object_to_click.click() def verify_ui_object_visible(ui_object, call_on_fail=None): """Verifies that a UI object is visible. Args: ui_object: A snippet-uiautomator selector object. call_on_fail: [Optional] Callable; method to call on failure. """ ui_object_visible = ui_object.wait.exists(UI_OBJECT_WAIT_TIME_SECONDS) if not ui_object_visible: if call_on_fail is not None: call_on_fail() raise AssertionError('UI object was not visible!') def open_jca_viewfinder(dut, log_path, request_video_capture=False): """Sends an intent to JCA and open its viewfinder. Args: dut: An Android controller device object. log_path: str; Log path to save screenshots. request_video_capture: boolean; True if requesting video capture. Raises: AssertionError: If JCA viewfinder is not visible. """ its_device_utils.start_its_test_activity(dut.serial) call_on_fail = lambda: dut.take_screenshot(log_path, prefix='its_not_found') verify_ui_object_visible( dut.ui(text=ITS_ACTIVITY_TEXT), call_on_fail=call_on_fail ) # Send intent to ItsTestActivity, which will start the correct JCA activity. if request_video_capture: its_device_utils.run( f'adb -s {dut.serial} shell am broadcast -a' f'{ACTION_ITS_DO_JCA_VIDEO_CAPTURE}' ) else: its_device_utils.run( f'adb -s {dut.serial} shell am broadcast -a' f'{ACTION_ITS_DO_JCA_CAPTURE}' ) jca_capture_button_visible = dut.ui( res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( UI_OBJECT_WAIT_TIME_SECONDS) if not jca_capture_button_visible: dut.take_screenshot(log_path, prefix=VIEWFINDER_NOT_VISIBLE_PREFIX) logging.debug('Current UI dump: %s', dut.ui.dump()) raise AssertionError('JCA was not started successfully!') dut.take_screenshot(log_path, prefix=VIEWFINDER_VISIBLE_PREFIX) def switch_jca_camera(dut, log_path, facing): """Interacts with JCA UI to switch camera if necessary. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. facing: str; constant describing the direction the camera lens faces. Raises: AssertionError: If JCA does not report that camera has been switched. """ if facing == camera_properties_utils.LENS_FACING['BACK']: ui_facing_description = UI_DESCRIPTION_BACK_CAMERA elif facing == camera_properties_utils.LENS_FACING['FRONT']: ui_facing_description = UI_DESCRIPTION_FRONT_CAMERA else: raise ValueError(f'Unknown facing: {facing}') dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() _find_ui_object_else_click(dut.ui(desc=ui_facing_description), dut.ui(res=QUICK_SET_FLIP_CAMERA_RESOURCE_ID)) if not dut.ui(desc=ui_facing_description).wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): dut.take_screenshot(log_path, prefix='failed_to_switch_camera') logging.debug('JCA UI dump: %s', dut.ui.dump()) raise AssertionError(f'Failed to switch to {ui_facing_description}!') dut.take_screenshot( log_path, prefix=f"switched_to_{ui_facing_description.replace(' ', '_')}" ) dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() def _get_current_flash_mode_desc(dut): """Returns the current flash mode description from the JCA UI.""" dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).wait.exists( UI_OBJECT_WAIT_TIME_SECONDS) return dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).child(depth=1).description def set_jca_flash_mode(dut, log_path, flash_mode_desc): """Interacts with JCA UI to set flash mode if necessary. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. flash_mode_desc: str; flash mode description to set. Acceptable values: FLASH_MODES Raises: AssertionError: If JCA fails to set the desired flash mode. """ if flash_mode_desc not in FLASH_MODES: raise ValueError( f'Invalid flash mode description: {flash_mode_desc}. ' f'Valid values: {FLASH_MODES}' ) dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() current_flash_mode_desc = _get_current_flash_mode_desc(dut) initial_flash_mode_desc = current_flash_mode_desc logging.debug('Initial flash mode description: %s', initial_flash_mode_desc) if initial_flash_mode_desc == flash_mode_desc: logging.debug('Initial flash mode %s matches desired flash mode %s', initial_flash_mode_desc, flash_mode_desc) else: while current_flash_mode_desc != flash_mode_desc: dut.ui(res=QUICK_SET_FLASH_RESOURCE_ID).click() current_flash_mode_desc = _get_current_flash_mode_desc(dut) if current_flash_mode_desc == initial_flash_mode_desc: raise AssertionError(f'Failed to set flash mode to {flash_mode_desc}!') if not dut.ui(desc=flash_mode_desc).wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): logging.debug('JCA UI dump: %s', dut.ui.dump()) dut.take_screenshot(log_path, prefix='cannot_set_flash_mode') raise AssertionError(f'Unable to confirm {flash_mode_desc} exists in UI') dut.take_screenshot(log_path, prefix='flash_mode_set') dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() def jca_ui_zoom(dut, zoom_ratio, log_path): """Interacts with the debug JCA overlay UI to zoom to the desired zoom ratio. Args: dut: An Android controller device object. zoom_ratio: float; zoom ratio desired. Will be rounded for compatibility. log_path: str; log path to save screenshots. Raises: AssertionError: If desired zoom ratio cannot be reached. """ zoom_ratio = round(zoom_ratio, 2) # JCA only supports 2 decimal places current_zoom_ratio_text = dut.ui(res=UI_ZOOM_RATIO_TEXT_RESOURCE_ID).text logging.debug('current zoom ratio text: %s', current_zoom_ratio_text) current_zoom_ratio = float(current_zoom_ratio_text[:-1]) # remove `x` if math.isclose(zoom_ratio, current_zoom_ratio): logging.debug('Desired zoom ratio is %.2f, ' 'current zoom ratio is %.2f. ' 'No need to zoom.', zoom_ratio, current_zoom_ratio) return dut.ui(res=UI_DEBUG_OVERLAY_BUTTON_RESOURCE_ID).click() dut.ui(res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_BUTTON_RESOURCE_ID).click() dut.ui( res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD_RESOURCE_ID ).set_text(str(zoom_ratio)) dut.ui(res=UI_DEBUG_OVERLAY_SET_ZOOM_RATIO_SET_BUTTON_RESOURCE_ID).click() # Ensure that preview is stable by clicking the center of the screen. center_x, center_y = ( dut.ui.info['displayWidth'] // 2, dut.ui.info['displayHeight'] // 2 ) dut.ui.click(x=center_x, y=center_y) time.sleep(UI_OBJECT_WAIT_TIME_SECONDS.total_seconds()) zoom_ratio_text_after_zoom = dut.ui(res=UI_ZOOM_RATIO_TEXT_RESOURCE_ID).text logging.debug('zoom ratio text after zoom: %s', zoom_ratio_text_after_zoom) zoom_ratio_after_zoom = float(zoom_ratio_text_after_zoom[:-1]) # remove `x` if not math.isclose(zoom_ratio, zoom_ratio_after_zoom): dut.take_screenshot( log_path, prefix=f'failed_to_zoom_to_{zoom_ratio}' ) raise AssertionError( f'Failed to zoom to {zoom_ratio}, ' f'zoomed to {zoom_ratio_after_zoom} instead.' ) logging.debug('Set zoom ratio to %.2f', zoom_ratio) dut.take_screenshot(log_path, prefix=f'zoomed_to_{zoom_ratio}') def change_jca_aspect_ratio(dut, log_path, aspect_ratio): """Interacts with JCA UI to change aspect ratio if necessary. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. aspect_ratio: str; Aspect ratio that JCA supports. Acceptable values: _RATIO_TO_UI_DESCRIPTION Raises: ValueError: If ratio is not supported in JCA. AssertionError: If JCA does not find the requested ratio. """ if aspect_ratio not in RATIO_TO_UI_DESCRIPTION: raise ValueError(f'Testing ratio {aspect_ratio} not supported in JCA!') dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() # Change aspect ratio in ratio switching menu if needed if not dut.ui(desc=aspect_ratio).wait.exists(UI_OBJECT_WAIT_TIME_SECONDS): dut.ui(res=QUICK_SET_RATIO_RESOURCE_ID).click() try: dut.ui(res=RATIO_TO_UI_DESCRIPTION[aspect_ratio]).click() except Exception as e: dut.take_screenshot( log_path, prefix=f'failed_to_find{aspect_ratio.replace(" ", "_")}' ) raise AssertionError( f'Testing ratio {aspect_ratio} not found in JCA app UI!') from e dut.ui(res=QUICK_SETTINGS_RESOURCE_ID).click() def do_jca_video_setup(dut, log_path, facing, aspect_ratio, stabilization_mode): """Change video capture settings using the UI. Selects UI elements to modify settings. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. facing: str; constant describing the direction the camera lens faces. Acceptable values: camera_properties_utils.LENS_FACING[BACK, FRONT] aspect_ratio: str; Aspect ratios that JCA supports. Acceptable values: _RATIO_TO_UI_DESCRIPTION stabilization_mode: int; constant describing the video stabilization mode. Acceptable values: 0, 1, 2 """ open_jca_viewfinder(dut, log_path, request_video_capture=True) switch_jca_camera(dut, log_path, facing) change_jca_aspect_ratio(dut, log_path, aspect_ratio) _set_jca_video_stabilization(dut, log_path, stabilization_mode) def _set_jca_video_stabilization(dut, log_path, stabilization_mode): """Change video stabilization mode using the UI. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. stabilization_mode: int; constant describing the video stabilization mode. Acceptable values: JCA_VIDEO_STABILIZATION_MODE_OFF, JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY, JCA_VIDEO_STABILIZATION_MODE_ON JCA_VIDEO_STABILIZATION_MODE_OPTICAL Mapping of JCA modes: ON: corresponds to setting android.control.videoStabilizationMode to PREVIEW_STABILIZATION. HIGH_QUALITY: corresponds to setting android.control.videoStabilizationMode to ON AUTO: will set the stabilization mode to PREVIEW_STABILIZATION, if the lens supports it, and if not, it will set it to OIS. If neither preview stabilization or OIS are supported it will be OFF. OPTICAL: optical stabilization is turned on in the default camera app when the video stabilization mode is OFF """ dut.ui(res=SETTINGS_BUTTON_RESOURCE_ID).click() if not dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): dut.take_screenshot( log_path, prefix='failed_to_find_video_stabilization_settings') raise AssertionError( 'Set Video Stabilization settings not found!' 'Make sure you have the latest JCA app.' ) dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).click() if not dut.ui(text=JCA_STABILIZATION_MODES[stabilization_mode]).wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): dut.take_screenshot( log_path, prefix='failed_to_find_video_stabilization_mode') raise AssertionError( 'Video Stabilization Mode not found!' ) # Ensure that the stabilzation options are enabled. # They will be disabled if the camera does not support stabilization if not dut.ui(text=SETTINGS_VIDEO_STABILIZATION_MODE_TEXT).enabled: raise AssertionError('Set Video Stabilization not enabled.') dut.ui(text=JCA_STABILIZATION_MODES[stabilization_mode]).click() time.sleep(ACTIVITY_WAIT_TIME_SECONDS) logging.debug('JCA Video Stabilization set to %s successfully.', JCA_STABILIZATION_MODES[stabilization_mode]) screenshot_prefix = ( f'jca_stabilization_mode_{JCA_STABILIZATION_MODES[stabilization_mode]}_set' ) dut.take_screenshot(log_path, prefix=screenshot_prefix) dut.ui(text=SETTINGS_CLOSE_TEXT).click() dut.ui(res=SETTINGS_BACK_BUTTON_RESOURCE_ID).click() # Verify that the setting was applied if stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_ON: if not dut.ui(desc='Preview is Stabilized').wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): raise AssertionError('JCA video stabilization_mode not set to ON.') elif stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_HIGH_QUALITY: if not dut.ui(desc='Only Video is Stabilized').wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): raise AssertionError( 'JCA video stabilization_mode not set to HIGH_QUALITY.') elif stabilization_mode == JCA_VIDEO_STABILIZATION_MODE_OPTICAL: if not dut.ui(desc='Optical stabilization is Enabled').wait.exists( UI_OBJECT_WAIT_TIME_SECONDS): raise AssertionError( 'JCA video stabilization_mode not set to OPTICAL.') else: if 'stabilize' in dut.ui.dump().lower(): raise AssertionError('JCA video stabilization_mode not set to OFF.') def default_camera_app_setup(device_id, pkg_name): """Setup Camera app by providing required permissions. Args: device_id: serial id of device. pkg_name: pkg name of the app to setup. Returns: Runtime exception from called function or None. """ logging.debug('Setting up the app with permission.') for permission in _PERMISSIONS_LIST: cmd = f'pm grant {pkg_name} android.permission.{permission}' its_device_utils.run_adb_shell_command(device_id, cmd) allow_manage_storage_cmd = ( f'appops set {pkg_name} MANAGE_EXTERNAL_STORAGE allow' ) its_device_utils.run_adb_shell_command(device_id, allow_manage_storage_cmd) def _get_current_camera_facing(content_desc, resource_id): """Returns the current camera facing based on UI elements.""" # If separator is present, the last element is the current camera facing. if DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR in content_desc: current_facing = content_desc.split( DEFAULT_CAMERA_CONTENT_DESC_SEPARATOR)[-1] if 'rear' in current_facing.lower() or 'back' in current_facing.lower(): return 'rear' elif 'front' in current_facing.lower(): return 'front' # If separator is not present, the element describes the other camera facing. if ('rear' in content_desc.lower() or 'rear' in resource_id.lower() or 'back' in content_desc.lower() or 'back' in resource_id.lower()): return 'front' elif 'front' in content_desc.lower() or 'front' in resource_id.lower(): return 'rear' else: raise ValueError('Failed to determine current camera facing.') def switch_default_camera(dut, facing, log_path): """Interacts with default camera app UI to switch camera. Args: dut: An Android controller device object. facing: str; constant describing the direction the camera lens faces. log_path: str; log path to save screenshots. Raises: AssertionError: If default camera app does not report that camera has been switched. """ flip_camera_pattern = ( r'(switch to|flip camera|switch camera|camera switch|' r'toggle_button|front_back_switcher|switch_camera_button|camera_switch_button)' ) flash_pattern = 'flash' default_ui_dump = dut.ui.dump() logging.debug('Default camera UI dump: %s', default_ui_dump) root = et.fromstring(default_ui_dump) for node in root.iter('node'): resource_id = node.get('resource-id') content_desc = node.get('content-desc') # Ignore resource ids for flash on/off if (re.search(flash_pattern, content_desc, re.IGNORECASE) or re.search(flash_pattern, resource_id, re.IGNORECASE)): continue if content_desc: if re.search( flip_camera_pattern, content_desc, re.IGNORECASE ): logging.debug('Pattern matches') logging.debug('Resource id: %s', resource_id) logging.debug('Flip camera content-desc: %s', content_desc) break else: if re.search( flip_camera_pattern, resource_id, re.IGNORECASE ): logging.debug('Pattern matches') logging.debug('Resource id: %s', resource_id) logging.debug('Flip camera content-desc: %s', content_desc) break else: raise AssertionError('Flip camera resource not found.') if facing == _get_current_camera_facing(content_desc, resource_id): logging.debug('Pattern found but camera is already switched.') else: if content_desc: dut.ui(desc=content_desc).click.wait() else: dut.ui(res=resource_id).click.wait() dut.take_screenshot( log_path, prefix=f'switched_to_{facing}_default_camera' ) def pull_img_files(device_id, input_path, output_path): """Pulls files from the input_path on the device to output_path. Args: device_id: serial id of device. input_path: File location on device. output_path: Location to save the file on the host. """ logging.debug('Pulling files from the device') pull_cmd = f'adb -s {device_id} pull {input_path} {output_path}' its_device_utils.run(pull_cmd) def launch_and_take_capture(dut, pkg_name, camera_facing, log_path, dumpsys_path=DEFAULT_CAMERA_APP_DUMPSYS_PATH): """Launches the camera app and takes still capture. Args: dut: An Android controller device object. pkg_name: pkg_name of the default camera app to be used for captures. camera_facing: camera lens facing orientation log_path: str; log path to save screenshots. dumpsys_path: path of the file on device to store the report Returns: img_path_on_dut: Path of the captured image on the device """ device_id = dut.serial # start cameraservice watch command to monitor default camera pkg watch_dump_path = os.path.join(log_path, DEFAULT_CAMERA_WATCH_DUMP_FILE) watch_process = start_cameraservice_watch(device_id, watch_dump_path, pkg_name) try: logging.debug('Launching app: %s', pkg_name) launch_cmd = f'monkey -p {pkg_name} 1' its_device_utils.run_adb_shell_command(device_id, launch_cmd) # Click OK/Done button on initial pop up windows if dut.ui(text=AGREE_BUTTON).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS): dut.ui(text=AGREE_BUTTON).click.wait() if dut.ui(text=AGREE_AND_CONTINUE_BUTTON).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS): dut.ui(text=AGREE_AND_CONTINUE_BUTTON).click.wait() if dut.ui(text=OK_BUTTON_TXT).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS): dut.ui(text=OK_BUTTON_TXT).click.wait() if dut.ui(text=DONE_BUTTON_TXT).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS): dut.ui(text=DONE_BUTTON_TXT).click.wait() if dut.ui(text=CANCEL_BUTTON_TXT).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS): dut.ui(text=CANCEL_BUTTON_TXT).click.wait() if dut.ui(text=LOCATION_ON_TXT).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS ): dut.ui(text=LOCATION_ON_TXT).click.wait() switch_default_camera(dut, camera_facing, log_path) take_dumpsys_report(dut, dumpsys_path) time.sleep(ACTIVITY_WAIT_TIME_SECONDS) logging.debug('Taking photo') its_device_utils.run_adb_shell_command(device_id, TAKE_PHOTO_CMD) # pull the dumpsys output dut.adb.pull([dumpsys_path, log_path]) time.sleep(ACTIVITY_WAIT_TIME_SECONDS) # stop cameraservice watch immediately after capturing image stop_cameraservice_watch(watch_process) img_path_on_dut = '' photo_storage_path = '' for path in CAMERA_FILES_PATHS: check_path_cmd = ( f'ls {path} && echo "Directory exists" || ' 'echo "Directory does not exist"' ) cmd_output = dut.adb.shell(check_path_cmd).decode('utf-8').strip() if _DIR_EXISTS_TXT in cmd_output: photo_storage_path = path break find_file_path = ( f'find {photo_storage_path} ! -empty -a ! -name \'.pending*\'' ' -a -type f -iname "*.jpg" -o -iname "*.jpeg"' ) img_path_on_dut = ( dut.adb.shell(find_file_path).decode('utf-8').strip().lower() ) logging.debug('Image path on DUT: %s', img_path_on_dut) if JPG_FORMAT_STR not in img_path_on_dut: raise AssertionError('Failed to find jpg files!') finally: force_stop_app(dut, pkg_name) return img_path_on_dut def restart_cts_verifier(dut, package_name): """Sends ADB commands to restart CtsVerifier app.""" # Set correct intent flags so that JCA finishes successfully (b/353830655) force_stop_app(dut, package_name) dut.adb.shell('am start -n com.android.cts.verifier/.CtsVerifierActivity') def force_stop_app(dut, pkg_name): """Force stops an app with given pkg_name. Args: dut: An Android controller device object. pkg_name: pkg_name of the app to be stopped. """ logging.debug('Closing app: %s', pkg_name) force_stop_cmd = f'am force-stop {pkg_name}' dut.adb.shell(force_stop_cmd) def default_camera_app_dut_setup(device_id, pkg_name): """Setup the device for testing default camera app. Args: device_id: serial id of device. pkg_name: pkg_name of the app. Returns: Runtime exception from called function or None. """ default_camera_app_setup(device_id, pkg_name) for path in CAMERA_FILES_PATHS: its_device_utils.run_adb_shell_command( device_id, f'{REMOVE_CAMERA_FILES_CMD} {path}/*') def launch_jca_and_capture(dut, log_path, camera_facing, zoom_ratio=None, video_stabilization=None): """Launches the jetpack camera app and takes still capture. Args: dut: An Android controller device object. log_path: str; log path to save screenshots. camera_facing: camera lens facing orientation zoom_ratio: optional; zoom_ratio to be set while taking the JCA capture. By default it will be set to 1 if the value is None. video_stabilization: optional; video stabilization mode to be set while taking the JCA capture. By default, JCA uses AUTO mode. AUTO in JCA will set the stabilization mode to PREVIEW_STABILIZATION, if the lens supports it, and if not, it will set it to OIS. If neither preview stabilization or OIS are supported it will be OFF. Returns: img_path_on_dut: Path of the captured image on the device """ device_id = dut.serial remove_command = f'rm -rf {EMULATED_STORAGE_PATH}/*' its_device_utils.run_adb_shell_command(device_id, remove_command) watch_dump_path = os.path.join(log_path, JCA_WATCH_DUMP_FILE) watch_process = start_cameraservice_watch(device_id, watch_dump_path, JETPACK_CAMERA_APP_PACKAGE_NAME) try: logging.debug('Launching JCA app') launch_cmd = ( 'am start -n ' f'{JETPACK_CAMERA_APP_PACKAGE_NAME}/{JETPACK_CAMERA_APP_PACKAGE_NAME}.MainActivity ' '--ez "KEY_DEBUG_MODE" true' ) its_device_utils.run_adb_shell_command(device_id, launch_cmd) switch_jca_camera(dut, log_path, camera_facing) change_jca_aspect_ratio(dut, log_path, aspect_ratio=THREE_TO_FOUR_ASPECT_RATIO_DESC) if video_stabilization is not None: _set_jca_video_stabilization(dut, log_path, video_stabilization) # Set zoom_ratio after setting video stabilization to avoid reset to default if zoom_ratio is not None: jca_ui_zoom(dut, zoom_ratio, log_path) # Take dumpsys before capturing the image take_dumpsys_report(dut, file_path=DEFAULT_JCA_UI_DUMPSYS_PATH) if dut.ui(res=CAPTURE_BUTTON_RESOURCE_ID).wait.exists( timeout=WAIT_INTERVAL_FIVE_SECONDS ): dut.ui(res=CAPTURE_BUTTON_RESOURCE_ID).click.wait() time.sleep(ACTIVITY_WAIT_TIME_SECONDS) stop_cameraservice_watch(watch_process) # pull the dumpsys output dut.adb.pull([DEFAULT_JCA_UI_DUMPSYS_PATH, log_path]) img_path_on_dut = ( dut.adb.shell( "find {} ! -empty -a ! -name '.pending*' -a -type f".format( EMULATED_STORAGE_PATH ) ) .decode('utf-8') .strip() ) logging.debug('Image path on DUT: %s', img_path_on_dut) if JPG_FORMAT_STR not in img_path_on_dut: raise AssertionError('Failed to find jpg files!') finally: force_stop_app(dut, JETPACK_CAMERA_APP_PACKAGE_NAME) return img_path_on_dut def take_dumpsys_report(dut, file_path): """Takes dumpsys report of camera service and stores the report in the file. Args: dut: An Android controller device object. file_path: Path of the file on device to store the report. """ dut.adb.shell(['dumpsys', 'media.camera', '>', file_path]) def _watch_clear(device_id): """Clears cameraservice watch cache. Args: device_id: serial id of device. """ cmd = f'adb -s {device_id} shell cmd media.camera watch clear'.split(' ') subprocess.run(cmd, check=True) logging.debug('Cleared watch cache') def _watch_start(device_id, pkg_name): """Starts cameraservice watch command. Args: device_id: serial id of device. pkg_name: pkg_name of the app. """ cmd = [ 'adb', '-s', device_id, 'shell', 'cmd', 'media.camera', 'watch', 'start', '-m', ( 'android.control.captureIntent,android.jpeg.quality,' 'android.control.zoomRatio,' 'android.scaler.cropRegion,' 'android.control.zoomMethod,' '3a' ), '-c', pkg_name, ] subprocess.run(cmd, check=True) logging.debug('Started watching 3a for %s', pkg_name) def _watch_live(device_id, file_path): """Starts cameraservice watch live command. Args: device_id: serial id of device. file_path: Path of the file to store the report. Returns: watch_process: subprocess.Popen object for the watch live command. """ cmd = f'adb -s {device_id} shell cmd media.camera watch live'.split(' ') with open(file_path, 'w') as f: logging.debug('Starting watch live') watch_process = subprocess.Popen( cmd, stdout=f, stdin=subprocess.PIPE ) logging.debug('watch live output written to the file_path: %s', file_path) return watch_process def start_cameraservice_watch(device_id, file_path, pkg_name): """Starts cameraservice watch command. Args: device_id: serial id of device. file_path: Path of the file to store the report. pkg_name: pkg_name of the app. Returns: watch_process: subprocess.Popen object for the watch live command. """ _watch_start(device_id, pkg_name) watch_process = _watch_live(device_id, file_path) watch_process.its_watch_process_device_id = device_id return watch_process def stop_cameraservice_watch(watch_process): """Stops cameraservice watch command. Args: watch_process: subprocess.Popen object returned by start_cameraservice_watch Raises: CameraItsError: If watch_process not created by start_cameraservice_watch """ if not hasattr(watch_process, 'its_watch_process_device_id'): raise error_util.CameraItsError( 'watch_process was not created by start_cameraservice_watch' ) device_id = watch_process.its_watch_process_device_id watch_process.stdin.write(b'\n') watch_process.stdin.flush() watch_process.wait() logging.debug('Stopping watch live') cmd = f'adb -s {device_id} shell cmd media.camera watch stop'.split(' ') subprocess.run(cmd, check=True) logging.debug('Stopped watching 3a') def get_default_camera_zoom_ratio(file_name): """Returns the zoom_ratio used by default camera capture. Args: file_name: str; file name storing default camera pkg watch cameraservice dump output. Returns: zoom_ratio: zoom_ratio rounded up to 2 decimal places Raises: FileNotFoundError: If file_name does not exist """ zoom_ratio_values = [] if not os.path.exists(file_name): raise FileNotFoundError(f'File not found: {file_name}') with open(file_name, 'r') as file: for line in file: if _CONTROL_ZOOM_RATIO_KEY in line: if _REQ_STR_PATTERN not in line: continue logging.debug('zoomRatio line: %s', line) values = line.split(':') value_str = values[-1] match = re.search(r'[\d.]+', value_str) if match: value = float(match.group()) rounded_value = round(value, 2) logging.debug('zoomRatio found: %s', rounded_value) zoom_ratio_values.append(rounded_value) if zoom_ratio_values: logging.debug('zoom_ratio_values: %s', zoom_ratio_values) return zoom_ratio_values[-1] return None def get_default_camera_video_stabilization(file_name): """Returns the video stabilization mode used by default camera capture. Args: file_name: str; file name storing default camera pkg watch cameraservice dump output. Returns: video_stabilization_mode: str; video stabilization mode used by default camera app during the capture Raises: FileNotFoundError: If file_name does not exist """ video_stabilization_modes = [] if not os.path.exists(file_name): raise FileNotFoundError(f'File not found: {file_name}') with open(file_name, 'r') as file: for line in file: if 'videoStabilizationMode' in line: logging.debug('videoStabilizationMode line: %s', line) values = line.split(':') value_str = values[-1] match = re.search(r'[a-zA-Z]+', value_str) if match: value = str(match.group()) logging.debug('videoStabilizationMode found: %s', value) video_stabilization_modes.append(value) if video_stabilization_modes: logging.debug('video_stabilization_modes: %s', video_stabilization_modes) logging.debug('videoStabilizationMode used for default captures: %s', video_stabilization_modes[-1]) return video_stabilization_modes[-1].strip() return None def get_default_camera_ois_mode(file_name): """Returns the optical stabilization mode used by default camera capture. Args: file_name: str; file name storing default camera pkg watch cameraservice dump output. Returns: optical_stabilization_mode: str; optical stabilization mode used by default camera app during the capture Raises: FileNotFoundError: If file_name does not exist """ optical_stabilization_modes = [] if not os.path.exists(file_name): raise FileNotFoundError(f'File not found: {file_name}') with open(file_name, 'r') as file: for line in file: if 'opticalStabilizationMode' in line: if _REQ_STR_PATTERN not in line: continue logging.debug('opticalStabilizationMode line: %s', line) values = line.split(':') value_str = values[-1] match = re.search(r'[a-zA-Z]+', value_str) if match: value = str(match.group()) logging.debug('opticalStabilizationMode found: %s', value) optical_stabilization_modes.append(value) if optical_stabilization_modes: logging.debug('optical_stabilization_modes: %s', optical_stabilization_modes) logging.debug('opticalStabilizationMode used for default captures: %s', optical_stabilization_modes[-1]) return optical_stabilization_modes[-1].strip() return None