blob: 163fd67901a7b779f8c96e63820feced3b700ce2 [file] [log] [blame]
# 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