• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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