From 515de7392ff90068d2f1f24b8f668b89171ea928 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Wed, 1 Apr 2026 22:22:37 +0000 Subject: [PATCH 1/9] Fix unsupported focus metering crash --- .../java/com/google/jetpackcamera/core/camera/FocusMetering.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 1d305ea23..3eca0cf77 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -98,6 +98,9 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( } } catch (_: CameraControl.OperationCanceledException) { FocusState.Status.FAILURE + } catch (e: IllegalArgumentException) { + Log.d(TAG, "tapToFocus failed: ${e.message}") + FocusState.Status.FAILURE } Log.d( From 4249029217606de04fcc2ec90f18cb88a455c57b Mon Sep 17 00:00:00 2001 From: davidjiagoogle <125502967+davidjiagoogle@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:28:59 -0700 Subject: [PATCH 2/9] Update core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../java/com/google/jetpackcamera/core/camera/FocusMetering.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 3eca0cf77..23e669237 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -99,7 +99,7 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( } catch (_: CameraControl.OperationCanceledException) { FocusState.Status.FAILURE } catch (e: IllegalArgumentException) { - Log.d(TAG, "tapToFocus failed: ${e.message}") + Log.w(TAG, "tapToFocus failed", e) FocusState.Status.FAILURE } From 472d3b8ad07a06fe1bc8026665a05cf74a50aa06 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Fri, 3 Apr 2026 18:33:38 +0000 Subject: [PATCH 3/9] Add unit test for IllegalArgumentException in focus metering --- .../core/camera/FocusMeteringUnitTest.kt | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt new file mode 100644 index 000000000..504fb66de --- /dev/null +++ b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 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. + */ +package com.google.jetpackcamera.core.camera + +import android.content.Context +import android.graphics.Matrix +import android.graphics.Rect +import android.util.Size +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraInfo +import androidx.camera.core.FocusMeteringAction +import androidx.camera.core.FocusMeteringResult +import androidx.camera.core.SurfaceRequest +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.ListenableFuture +import com.google.jetpackcamera.core.common.FilePathGenerator +import com.google.jetpackcamera.model.CameraEvent +import java.lang.reflect.Proxy +import java.util.concurrent.Executors +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class FocusMeteringUnitTest { + + private lateinit var context: Context + private lateinit var cameraSessionContext: CameraSessionContext + private lateinit var currentCameraState: MutableStateFlow + private lateinit var surfaceRequests: MutableStateFlow + private lateinit var focusMeteringEvents: Channel + private val testDispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + currentCameraState = MutableStateFlow(CameraState()) + surfaceRequests = MutableStateFlow(null) + focusMeteringEvents = Channel(Channel.UNLIMITED) + + cameraSessionContext = CameraSessionContext( + context = context, + cameraProvider = createFakeProxy(ProcessCameraProvider::class.java), + backgroundDispatcher = testDispatcher, + screenFlashEvents = Channel(), + filePathGenerator = createFakeProxy(FilePathGenerator::class.java), + focusMeteringEvents = focusMeteringEvents, + videoCaptureControlEvents = Channel(), + currentCameraState = currentCameraState, + surfaceRequests = surfaceRequests, + transientSettings = MutableStateFlow(null) + ) + } + + @Test + fun processFocusMeteringEvents_handlesIllegalArgumentException() = runTest(testDispatcher) { + // Arrange + val cameraInfo = createFakeProxy(CameraInfo::class.java) + + // We need a real SurfaceRequest to trigger the transformation info flow + val surfaceRequest = SurfaceRequest(Size(640, 480), createFakeProxy(CameraInfo::class.java)) { } + surfaceRequests.value = surfaceRequest + + val cameraControl = Proxy.newProxyInstance( + CameraControl::class.java.classLoader, + arrayOf(CameraControl::class.java) + ) { _, method, _ -> + if (method.name == "startFocusAndMetering") { + throw IllegalArgumentException("Test Exception") + } + null + } as CameraControl + + // Act + val job = launch { + with(cameraSessionContext) { + processFocusMeteringEvents(cameraInfo, cameraControl) + } + } + + // Trigger the transformation info flow + surfaceRequest.updateTransformationInfo( + SurfaceRequest.TransformationInfo.of( + Rect(0, 0, 640, 480), + 0, + 0, + false, + null + ) + ) + + advanceUntilIdle() + + // Send a focus event + focusMeteringEvents.trySend(CameraEvent.FocusMeteringEvent(0.5f, 0.5f)) + advanceUntilIdle() + + // Assert + val focusState = currentCameraState.value.focusState + assertThat(focusState).isInstanceOf(FocusState.Specified::class.java) + assertThat((focusState as FocusState.Specified).status).isEqualTo(FocusState.Status.FAILURE) + + job.cancel() + } + + @Suppress("UNCHECKED_CAST") + private fun createFakeProxy(clazz: Class): T { + return Proxy.newProxyInstance( + clazz.classLoader, + arrayOf(clazz) + ) { _, method, _ -> + when (method.name) { + "getSensorRect" -> Rect(0, 0, 1000, 1000) + "getSensorToBufferTransform" -> Matrix() + else -> null + } + } as T + } +} From 8b473b2a3473fbe17210d3cea70ba81767b191c7 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Fri, 3 Apr 2026 19:23:06 +0000 Subject: [PATCH 4/9] Improve FocusMeteringUnitTest: use real CameraX and simulated hardware camera --- .../core/camera/FocusMeteringUnitTest.kt | 76 +++++++++++++------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt index 504fb66de..1ce909188 100644 --- a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt +++ b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt @@ -18,19 +18,28 @@ package com.google.jetpackcamera.core.camera import android.content.Context import android.graphics.Matrix import android.graphics.Rect +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager import android.util.Size +import androidx.camera.camera2.Camera2Config import androidx.camera.core.CameraControl import androidx.camera.core.CameraInfo +import androidx.camera.core.CameraSelector import androidx.camera.core.FocusMeteringAction import androidx.camera.core.FocusMeteringResult import androidx.camera.core.SurfaceRequest +import androidx.camera.core.impl.CameraInfoInternal +import androidx.camera.core.impl.CameraInternal import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.ListenableFuture -import com.google.jetpackcamera.core.common.FilePathGenerator -import com.google.jetpackcamera.model.CameraEvent +import com.google.jetpackcamera.core.common.FakeFilePathGenerator +import com.google.jetpackcamera.core.camera.CameraEvent import java.lang.reflect.Proxy import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineDispatcher @@ -43,9 +52,21 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowCameraCharacteristics +import org.robolectric.shadows.ShadowCameraManager +import org.robolectric.shadows.ShadowLooper + +class FakeLifecycleOwner : LifecycleOwner { + private val registry = LifecycleRegistry(this).apply { + currentState = Lifecycle.State.RESUMED + } + override val lifecycle: Lifecycle get() = registry +} @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @@ -57,6 +78,7 @@ class FocusMeteringUnitTest { private lateinit var surfaceRequests: MutableStateFlow private lateinit var focusMeteringEvents: Channel private val testDispatcher = StandardTestDispatcher() + private lateinit var cameraProvider: ProcessCameraProvider @Before fun setUp() { @@ -65,12 +87,24 @@ class FocusMeteringUnitTest { surfaceRequests = MutableStateFlow(null) focusMeteringEvents = Channel(Channel.UNLIMITED) + // Setup simulated hardware camera for Robolectric + val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics() + val shadowCharacteristics = Shadows.shadowOf(characteristics) + shadowCharacteristics.set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK) + shadowCharacteristics.set(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, Rect(0, 0, 640, 480)) + + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + Shadows.shadowOf(cameraManager).addCamera("0", characteristics) + + ProcessCameraProvider.configureInstance(Camera2Config.defaultConfig()) + cameraProvider = ProcessCameraProvider.getInstance(context).get() + cameraSessionContext = CameraSessionContext( context = context, - cameraProvider = createFakeProxy(ProcessCameraProvider::class.java), + cameraProvider = cameraProvider, backgroundDispatcher = testDispatcher, screenFlashEvents = Channel(), - filePathGenerator = createFakeProxy(FilePathGenerator::class.java), + filePathGenerator = FakeFilePathGenerator(), focusMeteringEvents = focusMeteringEvents, videoCaptureControlEvents = Channel(), currentCameraState = currentCameraState, @@ -79,13 +113,19 @@ class FocusMeteringUnitTest { ) } + @After + fun tearDown() { + cameraProvider.unbindAll() + } + @Test fun processFocusMeteringEvents_handlesIllegalArgumentException() = runTest(testDispatcher) { // Arrange - val cameraInfo = createFakeProxy(CameraInfo::class.java) + val camera = cameraProvider.bindToLifecycle(FakeLifecycleOwner(), CameraSelector.DEFAULT_BACK_CAMERA) + val realCameraInfo = camera.cameraInfo // We need a real SurfaceRequest to trigger the transformation info flow - val surfaceRequest = SurfaceRequest(Size(640, 480), createFakeProxy(CameraInfo::class.java)) { } + val surfaceRequest = SurfaceRequest(Size(640, 480), camera as CameraInternal) { } surfaceRequests.value = surfaceRequest val cameraControl = Proxy.newProxyInstance( @@ -101,7 +141,7 @@ class FocusMeteringUnitTest { // Act val job = launch { with(cameraSessionContext) { - processFocusMeteringEvents(cameraInfo, cameraControl) + processFocusMeteringEvents(realCameraInfo, cameraControl) } } @@ -112,14 +152,18 @@ class FocusMeteringUnitTest { 0, 0, false, - null + Matrix(), + false ) ) + ShadowLooper.idleMainLooper() advanceUntilIdle() // Send a focus event focusMeteringEvents.trySend(CameraEvent.FocusMeteringEvent(0.5f, 0.5f)) + + ShadowLooper.idleMainLooper() advanceUntilIdle() // Assert @@ -129,18 +173,4 @@ class FocusMeteringUnitTest { job.cancel() } - - @Suppress("UNCHECKED_CAST") - private fun createFakeProxy(clazz: Class): T { - return Proxy.newProxyInstance( - clazz.classLoader, - arrayOf(clazz) - ) { _, method, _ -> - when (method.name) { - "getSensorRect" -> Rect(0, 0, 1000, 1000) - "getSensorToBufferTransform" -> Matrix() - else -> null - } - } as T - } -} +} \ No newline at end of file From 699de68d5d3405a1b6d73d7af177e1235516dd8f Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Fri, 3 Apr 2026 19:30:03 +0000 Subject: [PATCH 5/9] Delete FocusMeteringUnitTest and run spotless --- .../core/camera/FocusMeteringUnitTest.kt | 176 ------------------ 1 file changed, 176 deletions(-) delete mode 100644 core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt deleted file mode 100644 index 1ce909188..000000000 --- a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringUnitTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 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. - */ -package com.google.jetpackcamera.core.camera - -import android.content.Context -import android.graphics.Matrix -import android.graphics.Rect -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager -import android.util.Size -import androidx.camera.camera2.Camera2Config -import androidx.camera.core.CameraControl -import androidx.camera.core.CameraInfo -import androidx.camera.core.CameraSelector -import androidx.camera.core.FocusMeteringAction -import androidx.camera.core.FocusMeteringResult -import androidx.camera.core.SurfaceRequest -import androidx.camera.core.impl.CameraInfoInternal -import androidx.camera.core.impl.CameraInternal -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import com.google.common.util.concurrent.ListenableFuture -import com.google.jetpackcamera.core.common.FakeFilePathGenerator -import com.google.jetpackcamera.core.camera.CameraEvent -import java.lang.reflect.Proxy -import java.util.concurrent.Executors -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Shadows -import org.robolectric.shadows.ShadowCameraCharacteristics -import org.robolectric.shadows.ShadowCameraManager -import org.robolectric.shadows.ShadowLooper - -class FakeLifecycleOwner : LifecycleOwner { - private val registry = LifecycleRegistry(this).apply { - currentState = Lifecycle.State.RESUMED - } - override val lifecycle: Lifecycle get() = registry -} - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -class FocusMeteringUnitTest { - - private lateinit var context: Context - private lateinit var cameraSessionContext: CameraSessionContext - private lateinit var currentCameraState: MutableStateFlow - private lateinit var surfaceRequests: MutableStateFlow - private lateinit var focusMeteringEvents: Channel - private val testDispatcher = StandardTestDispatcher() - private lateinit var cameraProvider: ProcessCameraProvider - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - currentCameraState = MutableStateFlow(CameraState()) - surfaceRequests = MutableStateFlow(null) - focusMeteringEvents = Channel(Channel.UNLIMITED) - - // Setup simulated hardware camera for Robolectric - val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics() - val shadowCharacteristics = Shadows.shadowOf(characteristics) - shadowCharacteristics.set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK) - shadowCharacteristics.set(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, Rect(0, 0, 640, 480)) - - val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - Shadows.shadowOf(cameraManager).addCamera("0", characteristics) - - ProcessCameraProvider.configureInstance(Camera2Config.defaultConfig()) - cameraProvider = ProcessCameraProvider.getInstance(context).get() - - cameraSessionContext = CameraSessionContext( - context = context, - cameraProvider = cameraProvider, - backgroundDispatcher = testDispatcher, - screenFlashEvents = Channel(), - filePathGenerator = FakeFilePathGenerator(), - focusMeteringEvents = focusMeteringEvents, - videoCaptureControlEvents = Channel(), - currentCameraState = currentCameraState, - surfaceRequests = surfaceRequests, - transientSettings = MutableStateFlow(null) - ) - } - - @After - fun tearDown() { - cameraProvider.unbindAll() - } - - @Test - fun processFocusMeteringEvents_handlesIllegalArgumentException() = runTest(testDispatcher) { - // Arrange - val camera = cameraProvider.bindToLifecycle(FakeLifecycleOwner(), CameraSelector.DEFAULT_BACK_CAMERA) - val realCameraInfo = camera.cameraInfo - - // We need a real SurfaceRequest to trigger the transformation info flow - val surfaceRequest = SurfaceRequest(Size(640, 480), camera as CameraInternal) { } - surfaceRequests.value = surfaceRequest - - val cameraControl = Proxy.newProxyInstance( - CameraControl::class.java.classLoader, - arrayOf(CameraControl::class.java) - ) { _, method, _ -> - if (method.name == "startFocusAndMetering") { - throw IllegalArgumentException("Test Exception") - } - null - } as CameraControl - - // Act - val job = launch { - with(cameraSessionContext) { - processFocusMeteringEvents(realCameraInfo, cameraControl) - } - } - - // Trigger the transformation info flow - surfaceRequest.updateTransformationInfo( - SurfaceRequest.TransformationInfo.of( - Rect(0, 0, 640, 480), - 0, - 0, - false, - Matrix(), - false - ) - ) - - ShadowLooper.idleMainLooper() - advanceUntilIdle() - - // Send a focus event - focusMeteringEvents.trySend(CameraEvent.FocusMeteringEvent(0.5f, 0.5f)) - - ShadowLooper.idleMainLooper() - advanceUntilIdle() - - // Assert - val focusState = currentCameraState.value.focusState - assertThat(focusState).isInstanceOf(FocusState.Specified::class.java) - assertThat((focusState as FocusState.Specified).status).isEqualTo(FocusState.Status.FAILURE) - - job.cancel() - } -} \ No newline at end of file From 69b72678f51d5fb5a69c9a159cf4361fadc05864 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Tue, 14 Apr 2026 16:33:43 +0000 Subject: [PATCH 6/9] Fix: Check if focus metering is supported before initiating focus request --- .../core/camera/FocusMetering.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 23e669237..8900a9d72 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -85,30 +85,35 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( } } - updateFocusState(FocusState.Status.RUNNING) val meteringPoint = createPoint(event.x, event.y) val action = FocusMeteringAction.Builder(meteringPoint) .setAutoCancelDuration(AUTO_FOCUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) .build() - val completionStatus: FocusState.Status = try { - if (cameraControl.startFocusAndMetering(action).await().isFocusSuccessful) { - FocusState.Status.SUCCESS - } else { + + if (cameraInfo.isFocusMeteringSupported(action)) { + updateFocusState(FocusState.Status.RUNNING) + val completionStatus: FocusState.Status = try { + if (cameraControl.startFocusAndMetering(action).await().isFocusSuccessful) { + FocusState.Status.SUCCESS + } else { + FocusState.Status.FAILURE + } + } catch (_: CameraControl.OperationCanceledException) { + FocusState.Status.FAILURE + } catch (e: IllegalArgumentException) { + Log.w(TAG, "tapToFocus failed", e) FocusState.Status.FAILURE } - } catch (_: CameraControl.OperationCanceledException) { - FocusState.Status.FAILURE - } catch (e: IllegalArgumentException) { - Log.w(TAG, "tapToFocus failed", e) - FocusState.Status.FAILURE - } - Log.d( - TAG, - "tapToFocus, finished processing event: $event. Result: $completionStatus" - ) + Log.d( + TAG, + "tapToFocus, finished processing event: $event. Result: $completionStatus" + ) - updateFocusState(completionStatus) + updateFocusState(completionStatus) + } else { + Log.w(TAG, "Focus metering not supported for action: $action") + } } } } From a979860d057d7c1d992c5a8ef88a1bb12e092280 Mon Sep 17 00:00:00 2001 From: David Jia Date: Tue, 14 Apr 2026 09:38:14 -0700 Subject: [PATCH 7/9] spotltess --- .../com/google/jetpackcamera/core/camera/FocusMetering.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 8900a9d72..d22a49b54 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -93,7 +93,10 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( if (cameraInfo.isFocusMeteringSupported(action)) { updateFocusState(FocusState.Status.RUNNING) val completionStatus: FocusState.Status = try { - if (cameraControl.startFocusAndMetering(action).await().isFocusSuccessful) { + if (cameraControl.startFocusAndMetering( + action + ).await().isFocusSuccessful + ) { FocusState.Status.SUCCESS } else { FocusState.Status.FAILURE From a3401e42ded1d5a77aba31473c6288d10788a604 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Tue, 14 Apr 2026 18:00:13 +0000 Subject: [PATCH 8/9] Refactor: Use guard clause for focus metering support check and add unit test --- .../core/camera/FocusMetering.kt | 41 +++--- .../core/camera/FocusMeteringTest.kt | 128 ++++++++++++++++++ 2 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 8900a9d72..9253b8609 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -90,30 +90,31 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( .setAutoCancelDuration(AUTO_FOCUS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) .build() - if (cameraInfo.isFocusMeteringSupported(action)) { - updateFocusState(FocusState.Status.RUNNING) - val completionStatus: FocusState.Status = try { - if (cameraControl.startFocusAndMetering(action).await().isFocusSuccessful) { - FocusState.Status.SUCCESS - } else { - FocusState.Status.FAILURE - } - } catch (_: CameraControl.OperationCanceledException) { - FocusState.Status.FAILURE - } catch (e: IllegalArgumentException) { - Log.w(TAG, "tapToFocus failed", e) + if (!cameraInfo.isFocusMeteringSupported(action)) { + Log.w(TAG, "Focus metering not supported for action: $action") + return@apply + } + + updateFocusState(FocusState.Status.RUNNING) + val completionStatus: FocusState.Status = try { + if (cameraControl.startFocusAndMetering(action).await().isFocusSuccessful) { + FocusState.Status.SUCCESS + } else { FocusState.Status.FAILURE } + } catch (_: CameraControl.OperationCanceledException) { + FocusState.Status.FAILURE + } catch (e: IllegalArgumentException) { + Log.w(TAG, "tapToFocus failed", e) + FocusState.Status.FAILURE + } - Log.d( - TAG, - "tapToFocus, finished processing event: $event. Result: $completionStatus" - ) + Log.d( + TAG, + "tapToFocus, finished processing event: $event. Result: $completionStatus" + ) - updateFocusState(completionStatus) - } else { - Log.w(TAG, "Focus metering not supported for action: $action") - } + updateFocusState(completionStatus) } } } diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt new file mode 100644 index 000000000..218892372 --- /dev/null +++ b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 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. + */ +package com.google.jetpackcamera.core.camera + +import android.content.Context +import android.graphics.Matrix +import android.graphics.Rect +import android.util.Size +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraInfo +import androidx.camera.core.SurfaceRequest +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.common.FakeFilePathGenerator +import java.lang.reflect.Proxy +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class FocusMeteringTest { + + private lateinit var context: Context + private lateinit var cameraSessionContext: CameraSessionContext + private lateinit var currentCameraState: MutableStateFlow + private lateinit var surfaceRequests: MutableStateFlow + private lateinit var focusMeteringEvents: Channel + private val testDispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + currentCameraState = MutableStateFlow(CameraState()) + surfaceRequests = MutableStateFlow(null) + focusMeteringEvents = Channel(Channel.UNLIMITED) + + cameraSessionContext = CameraSessionContext( + context = context, + cameraProvider = createFakeProxy(ProcessCameraProvider::class.java), + backgroundDispatcher = testDispatcher, + screenFlashEvents = Channel(), + filePathGenerator = FakeFilePathGenerator(), + focusMeteringEvents = focusMeteringEvents, + videoCaptureControlEvents = Channel(), + currentCameraState = currentCameraState, + surfaceRequests = surfaceRequests, + transientSettings = MutableStateFlow(null) + ) + } + + @Test + fun processFocusMeteringEvents_whenNotSupported_doesNotUpdateState() = runTest(testDispatcher) { + // Arrange + val cameraInfo = createFakeProxy(CameraInfo::class.java, isFocusSupported = false) + val surfaceRequest = SurfaceRequest(Size(640, 480), createFakeProxy(CameraInfo::class.java)) { } + surfaceRequests.value = surfaceRequest + + val cameraControl = createFakeProxy(CameraControl::class.java) + + // Act + val job = launch { + with(cameraSessionContext) { + processFocusMeteringEvents(cameraInfo, cameraControl) + } + } + + // Trigger the transformation info flow + surfaceRequest.updateTransformationInfo( + SurfaceRequest.TransformationInfo.of( + Rect(0, 0, 640, 480), + 0, + 0, + false, + Matrix(), + false + ) + ) + advanceUntilIdle() + + // Send a focus event + focusMeteringEvents.trySend(CameraEvent.FocusMeteringEvent(0.5f, 0.5f)) + advanceUntilIdle() + + // Assert + val focusState = currentCameraState.value.focusState + assertThat(focusState).isInstanceOf(FocusState.Unspecified::class.java) + + job.cancel() + } + + @Suppress("UNCHECKED_CAST") + private fun createFakeProxy(clazz: Class, isFocusSupported: Boolean = true): T { + return Proxy.newProxyInstance( + clazz.classLoader, + arrayOf(clazz) + ) { _, method, _ -> + when (method.name) { + "getSensorRect" -> Rect(0, 0, 1000, 1000) + "getSensorToBufferTransform" -> Matrix() + "isFocusMeteringSupported" -> isFocusSupported + else -> null + } + } as T + } +} From a651a9d5a511bf53835b4d372468631d054e19c0 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Tue, 14 Apr 2026 18:31:16 +0000 Subject: [PATCH 9/9] Remove FocusMeteringTest.kt --- .../core/camera/FocusMeteringTest.kt | 128 ------------------ 1 file changed, 128 deletions(-) delete mode 100644 core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt b/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt deleted file mode 100644 index 218892372..000000000 --- a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/FocusMeteringTest.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 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. - */ -package com.google.jetpackcamera.core.camera - -import android.content.Context -import android.graphics.Matrix -import android.graphics.Rect -import android.util.Size -import androidx.camera.core.CameraControl -import androidx.camera.core.CameraInfo -import androidx.camera.core.SurfaceRequest -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.core.common.FakeFilePathGenerator -import java.lang.reflect.Proxy -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -class FocusMeteringTest { - - private lateinit var context: Context - private lateinit var cameraSessionContext: CameraSessionContext - private lateinit var currentCameraState: MutableStateFlow - private lateinit var surfaceRequests: MutableStateFlow - private lateinit var focusMeteringEvents: Channel - private val testDispatcher = StandardTestDispatcher() - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - currentCameraState = MutableStateFlow(CameraState()) - surfaceRequests = MutableStateFlow(null) - focusMeteringEvents = Channel(Channel.UNLIMITED) - - cameraSessionContext = CameraSessionContext( - context = context, - cameraProvider = createFakeProxy(ProcessCameraProvider::class.java), - backgroundDispatcher = testDispatcher, - screenFlashEvents = Channel(), - filePathGenerator = FakeFilePathGenerator(), - focusMeteringEvents = focusMeteringEvents, - videoCaptureControlEvents = Channel(), - currentCameraState = currentCameraState, - surfaceRequests = surfaceRequests, - transientSettings = MutableStateFlow(null) - ) - } - - @Test - fun processFocusMeteringEvents_whenNotSupported_doesNotUpdateState() = runTest(testDispatcher) { - // Arrange - val cameraInfo = createFakeProxy(CameraInfo::class.java, isFocusSupported = false) - val surfaceRequest = SurfaceRequest(Size(640, 480), createFakeProxy(CameraInfo::class.java)) { } - surfaceRequests.value = surfaceRequest - - val cameraControl = createFakeProxy(CameraControl::class.java) - - // Act - val job = launch { - with(cameraSessionContext) { - processFocusMeteringEvents(cameraInfo, cameraControl) - } - } - - // Trigger the transformation info flow - surfaceRequest.updateTransformationInfo( - SurfaceRequest.TransformationInfo.of( - Rect(0, 0, 640, 480), - 0, - 0, - false, - Matrix(), - false - ) - ) - advanceUntilIdle() - - // Send a focus event - focusMeteringEvents.trySend(CameraEvent.FocusMeteringEvent(0.5f, 0.5f)) - advanceUntilIdle() - - // Assert - val focusState = currentCameraState.value.focusState - assertThat(focusState).isInstanceOf(FocusState.Unspecified::class.java) - - job.cancel() - } - - @Suppress("UNCHECKED_CAST") - private fun createFakeProxy(clazz: Class, isFocusSupported: Boolean = true): T { - return Proxy.newProxyInstance( - clazz.classLoader, - arrayOf(clazz) - ) { _, method, _ -> - when (method.name) { - "getSensorRect" -> Rect(0, 0, 1000, 1000) - "getSensorToBufferTransform" -> Matrix() - "isFocusMeteringSupported" -> isFocusSupported - else -> null - } - } as T - } -}