1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.camera2.internal 18 19 import android.hardware.camera2.CameraCharacteristics 20 import android.hardware.camera2.CaptureRequest 21 import android.hardware.camera2.CaptureResult 22 import android.os.Looper.getMainLooper 23 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat 24 import androidx.camera.camera2.internal.compat.quirk.TorchFlashRequiredFor3aUpdateQuirk 25 import androidx.camera.camera2.internal.compat.workaround.UseFlashModeTorchFor3aUpdate 26 import androidx.camera.core.ImageCapture 27 import androidx.camera.core.ImageCapture.ScreenFlash 28 import androidx.camera.core.impl.CameraControlInternal 29 import androidx.camera.core.impl.CaptureConfig 30 import androidx.camera.core.impl.Quirk 31 import androidx.camera.core.impl.Quirks 32 import androidx.camera.testing.impl.mocks.MockScreenFlash 33 import androidx.testutils.assertThrows 34 import com.google.common.truth.Truth.assertThat 35 import com.google.common.util.concurrent.MoreExecutors 36 import java.util.concurrent.Executors 37 import java.util.concurrent.Future 38 import java.util.concurrent.TimeUnit 39 import java.util.concurrent.TimeoutException 40 import org.junit.After 41 import org.junit.Test 42 import org.junit.runner.RunWith 43 import org.robolectric.RobolectricTestRunner 44 import org.robolectric.Shadows.shadowOf 45 import org.robolectric.annotation.Config 46 import org.robolectric.annotation.internal.DoNotInstrument 47 import org.robolectric.shadow.api.Shadow 48 import org.robolectric.shadows.ShadowCameraCharacteristics 49 import org.robolectric.shadows.ShadowTotalCaptureResult 50 51 @Config(minSdk = 21) 52 @RunWith(RobolectricTestRunner::class) 53 @DoNotInstrument 54 class ScreenFlashTaskTest { 55 private val executorService = Executors.newSingleThreadScheduledExecutor() 56 57 private var cameraCharacteristics = createCameraCharacteristicsCompat() 58 private val screenFlash = MockScreenFlash() 59 60 private lateinit var cameraControl: FakeCamera2CameraControlImpl 61 62 @After 63 fun tearDown() { 64 executorService.shutdown() 65 } 66 67 // 3s timeout is hardcoded in ImageCapture.ScreenFlash.apply documentation 68 @Test 69 fun screenFlashApplyInvokedWithAtLeast3sTimeout_whenPreCaptureCalled() { 70 val screenFlashTask = createScreenFlashTask() 71 val initialTime = System.currentTimeMillis() 72 73 screenFlashTask.preCapture(null) 74 shadowOf(getMainLooper()).idle() 75 76 assertThat(screenFlash.lastApplyExpirationTimeMillis) 77 .isAtLeast(initialTime + TimeUnit.SECONDS.toMillis(3)) 78 } 79 80 @Test 81 fun screenFlashApplyInvokedWithLessThan4sTimeout_whenPreCaptureCalled() { 82 val screenFlashTask = createScreenFlashTask() 83 val initialTime = System.currentTimeMillis() 84 85 screenFlashTask.preCapture(null) 86 shadowOf(getMainLooper()).idle() 87 88 assertThat(screenFlash.lastApplyExpirationTimeMillis) 89 .isLessThan(initialTime + TimeUnit.SECONDS.toMillis(4)) 90 } 91 92 @Test 93 fun screenFlashApplyInvokedInMainThread_whenPreCaptureCalled() { 94 val screenFlashTask = createScreenFlashTask() 95 96 screenFlashTask.preCapture(null) 97 shadowOf(getMainLooper()).idle() 98 99 assertThat(screenFlash.lastApplyThreadLooper).isEqualTo(getMainLooper()) 100 } 101 102 @Config(minSdk = 28) 103 @Test 104 fun externalFlashAeModeEnabled_whenPreCaptureCalled() { 105 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = true) 106 val screenFlashTask = createScreenFlashTask() 107 108 screenFlashTask.preCapture(null) 109 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 110 111 assertThat(cameraControl.focusMeteringControl.externalFlashAeModeEnabled).isEqualTo(true) 112 } 113 114 @Config(minSdk = 28) 115 @Test 116 fun externalFlashAeModeEnabled_whenScreenFlashApplyNotCompleted() { 117 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = true) 118 val screenFlashTask = createScreenFlashTask() 119 screenFlash.setApplyCompletedInstantly(false) 120 121 screenFlashTask.preCapture(null) 122 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 123 124 assertThat(cameraControl.focusMeteringControl.externalFlashAeModeEnabled).isEqualTo(true) 125 } 126 127 @Test 128 fun torchNotEnabled_whenPreCaptureCalledWithoutQuirk() { 129 val screenFlashTask = createScreenFlashTask(addTorchFlashRequiredQuirk = false) 130 131 screenFlashTask.preCapture(null) 132 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 133 134 assertThat(cameraControl.isTorchEnabled).isFalse() 135 } 136 137 @Config(minSdk = 28) 138 @Test 139 fun torchNotEnabled_whenPreCaptureCalledWithQuirk_butExternalFlashAeModeSupported() { 140 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = true) 141 val screenFlashTask = createScreenFlashTask(addTorchFlashRequiredQuirk = true) 142 143 screenFlashTask.preCapture(null) 144 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 145 146 assertThat(cameraControl.isTorchEnabled).isFalse() 147 } 148 149 @Test 150 fun torchEnabled_whenPreCaptureCalledWithQuirk_andNoExternalFlashAeMode() { 151 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = false) 152 val screenFlashTask = createScreenFlashTask(addTorchFlashRequiredQuirk = true) 153 154 screenFlashTask.preCapture(null) 155 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 156 157 assertThat(cameraControl.isTorchEnabled).isTrue() 158 } 159 160 @Test 161 fun torchEnabled_whenScreenFlashApplyNotCompleted() { 162 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = false) 163 val screenFlashTask = createScreenFlashTask(addTorchFlashRequiredQuirk = true) 164 screenFlash.setApplyCompletedInstantly(false) 165 166 screenFlashTask.preCapture(null) 167 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 168 169 assertThat(cameraControl.isTorchEnabled).isTrue() 170 } 171 172 @Test 173 fun aePrecaptureTriggered_whenPreCaptureCalled() { 174 val screenFlashTask = createScreenFlashTask() 175 176 screenFlashTask.preCapture(null) 177 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 178 179 assertThat(cameraControl.focusMeteringControl.triggerAePrecaptureCount).isEqualTo(1) 180 } 181 182 @Test 183 fun aePrecaptureNotTriggeredUntilTimeout_whenScreenFlashApplyNotCompleted() { 184 val screenFlashTask = createScreenFlashTask() 185 screenFlash.setApplyCompletedInstantly(false) 186 187 screenFlashTask.preCapture(null) 188 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 189 190 cameraControl.focusMeteringControl.awaitTriggerAePrecapture(1000) 191 } 192 193 @Test 194 fun aePrecaptureTriggeredAfterTimeout_whenScreenFlashApplyNotCompleted() { 195 val screenFlashTask = createScreenFlashTask() 196 screenFlash.setApplyCompletedInstantly(false) 197 198 screenFlashTask.preCapture(null) 199 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 200 201 cameraControl.focusMeteringControl.awaitTriggerAePrecapture( 202 TimeUnit.SECONDS.toMillis(ImageCapture.SCREEN_FLASH_UI_APPLY_TIMEOUT_SECONDS + 1) 203 ) 204 } 205 206 @Test 207 fun preCaptureIncompleteUntilTimeout_without3aConverge() { 208 val screenFlashTask = createScreenFlashTask() 209 210 val future = screenFlashTask.preCapture(null) 211 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 212 213 future.awaitException(1000, TimeoutException::class.java) 214 } 215 216 @Test 217 fun preCaptureCompletes_when3aConverges() { 218 val screenFlashTask = createScreenFlashTask() 219 220 val future = screenFlashTask.preCapture(null) 221 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 222 223 cameraControl.notifyCaptureResultListeners(RESULT_CONVERGED) 224 225 future.await(1000) 226 } 227 228 @Test 229 fun preCaptureCompletesByTimeout_without3aConverge() { 230 val screenFlashTask = createScreenFlashTask() 231 232 val future = screenFlashTask.preCapture(null) 233 shadowOf(getMainLooper()).idle() // ScreenFlash#apply is invoked in main thread 234 235 future.await(3000) 236 } 237 238 @Test 239 fun screenFlashClearInvokedInMainThread_whenPostCaptureCalled() { 240 val screenFlashTask = createScreenFlashTask() 241 242 screenFlashTask.postCapture() 243 shadowOf(getMainLooper()).idle() 244 245 assertThat(screenFlash.lastClearThreadLooper).isEqualTo(getMainLooper()) 246 } 247 248 @Test 249 fun torchDisabled_whenPostCaptureCalledWithQuirk_andNoExternalFlashAeMode() { 250 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = false) 251 val screenFlashTask = createScreenFlashTask(addTorchFlashRequiredQuirk = true) 252 screenFlashTask.preCapture(null) 253 254 screenFlashTask.postCapture() 255 256 assertThat(cameraControl.isTorchEnabled).isFalse() 257 } 258 259 @Config(minSdk = 28) 260 @Test 261 fun externalFlashAeModeDisabled_whenPostCaptureCalled() { 262 cameraCharacteristics = createCameraCharacteristicsCompat(addExternalFlashAeMode = true) 263 val screenFlashTask = createScreenFlashTask() 264 screenFlashTask.preCapture(null) 265 266 screenFlashTask.postCapture() 267 268 assertThat(cameraControl.focusMeteringControl.externalFlashAeModeEnabled).isEqualTo(false) 269 } 270 271 @Test 272 fun afAeTriggerCancelled_whenPostCaptureCalled() { 273 val screenFlashTask = createScreenFlashTask() 274 275 screenFlashTask.postCapture() 276 shadowOf(getMainLooper()).idle() 277 278 assertThat(cameraControl.focusMeteringControl.cancelAfAeTriggerCount).isEqualTo(1) 279 } 280 281 private fun createScreenFlashTask( 282 addTorchFlashRequiredQuirk: Boolean = false 283 ): Camera2CapturePipeline.ScreenFlashTask { 284 val quirks = 285 Quirks( 286 mutableListOf<Quirk>().apply { 287 if (addTorchFlashRequiredQuirk) { 288 add(TorchFlashRequiredFor3aUpdateQuirk(cameraCharacteristics)) 289 } 290 } 291 ) 292 293 cameraControl = FakeCamera2CameraControlImpl(cameraCharacteristics, quirks, screenFlash) 294 return Camera2CapturePipeline.ScreenFlashTask( 295 cameraControl, 296 MoreExecutors.directExecutor(), 297 executorService, 298 UseFlashModeTorchFor3aUpdate(quirks), 299 ) 300 } 301 302 private fun createCameraCharacteristicsCompat( 303 addExternalFlashAeMode: Boolean = false, 304 ) = 305 CameraCharacteristicsCompat.toCameraCharacteristicsCompat( 306 ShadowCameraCharacteristics.newCameraCharacteristics().also { 307 Shadow.extract<ShadowCameraCharacteristics>(it).apply { 308 if (addExternalFlashAeMode) { 309 set( 310 CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES, 311 intArrayOf(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) 312 ) 313 } 314 } 315 }, 316 CAMERA_ID 317 ) 318 319 internal inner class FakeCamera2CameraControlImpl( 320 cameraCharacteristics: CameraCharacteristicsCompat = createCameraCharacteristicsCompat(), 321 quirks: Quirks, 322 private val screenFlash: ScreenFlash, 323 ) : 324 Camera2CameraControlImpl( 325 cameraCharacteristics, 326 executorService, 327 MoreExecutors.directExecutor(), 328 object : CameraControlInternal.ControlUpdateCallback { 329 override fun onCameraControlUpdateSessionConfig() {} 330 331 override fun onCameraControlCaptureRequests( 332 captureConfigs: MutableList<CaptureConfig> 333 ) {} 334 } 335 ) { 336 private lateinit var captureResultListeners: MutableList<CaptureResultListener> 337 var isTorchEnabled = false 338 339 private val focusMeteringControl = FakeFocusMeteringControl(this, quirks) 340 341 init { 342 // can be called from super class constructor 343 if (!::captureResultListeners.isInitialized) { 344 captureResultListeners = mutableListOf() 345 } 346 } 347 348 override fun getFocusMeteringControl(): FakeFocusMeteringControl { 349 return focusMeteringControl 350 } 351 352 override fun getScreenFlash(): ScreenFlash { 353 return screenFlash 354 } 355 356 override fun enableTorchInternal(torchState: Int) { 357 isTorchEnabled = torchState != TorchControl.OFF 358 } 359 360 override fun addCaptureResultListener(listener: CaptureResultListener) { 361 // can be called from super class constructor 362 if (!::captureResultListeners.isInitialized) { 363 captureResultListeners = mutableListOf() 364 } 365 captureResultListeners.add(listener) 366 } 367 368 @Suppress("UNCHECKED_CAST") 369 fun notifyCaptureResultListeners(resultParameters: Map<CaptureResult.Key<*>, *>) { 370 captureResultListeners.forEach { listener -> 371 val shadowCaptureResult = ShadowTotalCaptureResult() 372 373 resultParameters.forEach { (k, v) -> 374 shadowCaptureResult.set(k as CaptureResult.Key<Any>, v as Any) 375 } 376 377 listener.onCaptureResult(ShadowTotalCaptureResult.newTotalCaptureResult()) 378 } 379 } 380 } 381 382 private fun Future<*>.await(timeoutMillis: Long) = get(timeoutMillis, TimeUnit.MILLISECONDS) 383 384 private fun <T : Throwable?> Future<*>.awaitException( 385 timeoutMillis: Long, 386 exceptionType: Class<T> 387 ) { 388 assertThrows(exceptionType) { get(timeoutMillis, TimeUnit.MILLISECONDS) } 389 } 390 391 companion object { 392 private const val CAMERA_ID = "0" 393 394 private val RESULT_CONVERGED: Map<CaptureResult.Key<*>, *> = 395 mapOf( 396 CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO, 397 CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, 398 CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED, 399 CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_CONVERGED, 400 ) 401 } 402 } 403