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