# Copyright 2025 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. """Verify Night Mode Indicator works correctly during camera use.""" import logging import os.path from mobly import test_runner import camera_properties_utils import preview_processing_utils import its_base_test import its_session_utils import lighting_control_utils import numpy as np import cv2 _TEST_NAME = os.path.splitext(os.path.basename(__file__))[0] _EXTENSION_NONE = -1 _EXTENSION_NIGHT = 4 # CameraExtensionCharacteristics.EXTENSION_NIGHT _SESSION_TYPE_CAMERA2 = 'camera2' _SESSION_TYPE_CAMERA_EXTENSION = 'cameraExtension' _CAMERA_SESSION_TYPES = (_SESSION_TYPE_CAMERA2, _SESSION_TYPE_CAMERA_EXTENSION) _NUM_FRAMES_TO_WAIT = 10 _CONTROL_MODE_AUTO = 1 _CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0 _CAPTURE_REQUEST = { 'android.control.mode': _CONTROL_MODE_AUTO, 'android.control.videoStabilizationMode': _CONTROL_VIDEO_STABILIZATION_MODE_OFF, } _CAPTURE_RESULT_KEY_NIGHT_MODE_INDICATOR = 'android.extension.nightModeIndicator' def _start_preview(cam, file_stem, camera_id, target_preview_size, camera_session_type): """Start a preview capture session and verify the Night Mode Indicator state. Args: cam: ItsSession object file_stem: string, prefix for the output image file camera_id: string, camera ID to use for capture target_preview_size: tuple, (width, height) of the preview size to use camera_session_type: string, either 'camera2' or 'cameraExtension' """ extension = _EXTENSION_NONE if camera_session_type == _SESSION_TYPE_CAMERA_EXTENSION: extension = _EXTENSION_NIGHT metadata, frame_bytes = cam.do_capture_preview_frame( camera_id, target_preview_size, _NUM_FRAMES_TO_WAIT, extension, _CAPTURE_REQUEST ) np_array = np.frombuffer(frame_bytes, dtype=np.uint8) img_rgb = cv2.imdecode(np_array, cv2.IMREAD_COLOR) cv2.imwrite(f'{file_stem}_capture.jpg', img_rgb) if metadata is None: raise AssertionError('No metadata returned from capture') if metadata[_CAPTURE_RESULT_KEY_NIGHT_MODE_INDICATOR] is None: raise AssertionError('No Night Mode Indicator value in metadata') result = metadata[_CAPTURE_RESULT_KEY_NIGHT_MODE_INDICATOR] logging.debug('Night Mode Indicator value: %s', result) class NightModeIndicatorTest(its_base_test.ItsBaseTest): """Test that Night Mode Indicator works as intended. This feature is only available on devices that also support Night Mode Camera Extensions. If Night Mode is supported and the feature is present in the available capture result keys, then the this test will setup a repeating capture request for the preview stream in both a Camera2 session and a Camera Extensions session. The test will then verify that the Night Mode Indicator changes state as expected during the capture session -- when the light is turned off, the indicator should change to the "ON" state; when the light is turned back on, the indicator should change to the "OFF" state. """ def test_night_mode_indicator(self): with its_session_utils.ItsSession( device_id=self.dut.serial, camera_id=self.camera_id, hidden_physical_id=self.hidden_physical_id) as cam: props = cam.get_camera_properties() cam.override_with_hidden_physical_camera_props(props) # check SKIP conditions first_api_level = its_session_utils.get_first_api_level(self.dut.serial) camera_properties_utils.skip_unless( first_api_level >= its_session_utils.ANDROID16_API_LEVEL and cam.is_night_mode_indicator_supported(self.camera_id) ) # establish connection with lighting controller arduino_serial_port = lighting_control_utils.lighting_control( self.lighting_cntl, self.lighting_ch ) for session_type in _CAMERA_SESSION_TYPES: logging.debug('scenario: %s', session_type) file_stem = f'{_TEST_NAME}_{session_type}' target_preview_size = None if session_type == _SESSION_TYPE_CAMERA2: target_preview_size = ( preview_processing_utils.get_max_preview_test_size( cam, self.camera_id ) ) elif session_type == _SESSION_TYPE_CAMERA_EXTENSION: target_preview_size = ( preview_processing_utils.get_max_extension_preview_test_size( cam, self.camera_id, _EXTENSION_NIGHT ) ) else: logging.debug('Unknown scenario: %s', session_type) continue lighting_control_utils.set_lighting_state( arduino_serial_port, self.lighting_ch, 'ON') result = _start_preview( cam, file_stem, self.camera_id, target_preview_size, session_type) if (result != 'OFF'): raise AssertionError('Lighting state ON did not result in Night Mode ' 'Indicator state OFF.') lighting_control_utils.set_lighting_state( arduino_serial_port, self.lighting_ch, 'OFF') result = _start_preview( cam, file_stem, self.camera_id, target_preview_size, session_type) if (result != 'ON'): raise AssertionError('Lighting state OFF did not result in Night ' 'Mode Indicator state ON.') if __name__ == '__main__': test_runner.main()