1 /* 2 * Copyright (C) 2019 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.testing.fakes; 18 19 import android.text.TextUtils; 20 import android.view.Surface; 21 22 import androidx.annotation.IntRange; 23 import androidx.annotation.RestrictTo; 24 import androidx.camera.core.CameraState; 25 import androidx.camera.core.Logger; 26 import androidx.camera.core.UseCase; 27 import androidx.camera.core.impl.CameraConfig; 28 import androidx.camera.core.impl.CameraConfigs; 29 import androidx.camera.core.impl.CameraControlInternal; 30 import androidx.camera.core.impl.CameraInfoInternal; 31 import androidx.camera.core.impl.CameraInternal; 32 import androidx.camera.core.impl.CaptureConfig; 33 import androidx.camera.core.impl.DeferrableSurface; 34 import androidx.camera.core.impl.DeferrableSurfaces; 35 import androidx.camera.core.impl.LiveDataObservable; 36 import androidx.camera.core.impl.Observable; 37 import androidx.camera.core.impl.SessionConfig; 38 import androidx.camera.core.impl.UseCaseAttachState; 39 import androidx.camera.core.impl.utils.executor.CameraXExecutors; 40 import androidx.camera.core.impl.utils.futures.FutureCallback; 41 import androidx.camera.core.impl.utils.futures.Futures; 42 import androidx.camera.testing.impl.CaptureSimulationKt; 43 import androidx.core.util.Preconditions; 44 45 import com.google.common.util.concurrent.ListenableFuture; 46 47 import org.jspecify.annotations.NonNull; 48 import org.jspecify.annotations.Nullable; 49 50 import java.util.ArrayList; 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Set; 56 import java.util.concurrent.ExecutionException; 57 import java.util.concurrent.Executor; 58 import java.util.concurrent.TimeUnit; 59 import java.util.concurrent.TimeoutException; 60 61 /** 62 * A fake camera which will not produce any data, but provides a valid Camera implementation. 63 */ 64 public class FakeCamera implements CameraInternal { 65 private static final String TAG = "FakeCamera"; 66 private static final String DEFAULT_CAMERA_ID = "0"; 67 private static final long TIMEOUT_GET_SURFACE_IN_MS = 5000L; 68 private final LiveDataObservable<CameraInternal.State> mObservableState = 69 new LiveDataObservable<>(); 70 private final CameraControlInternal mCameraControlInternal; 71 private final CameraInfoInternal mCameraInfoInternal; 72 private final String mCameraId; 73 private final UseCaseAttachState mUseCaseAttachState; 74 private final Set<UseCase> mAttachedUseCases = new HashSet<>(); 75 private State mState = State.CLOSED; 76 private int mAvailableCameraCount = 1; 77 private final List<UseCase> mUseCaseActiveHistory = new ArrayList<>(); 78 private final List<UseCase> mUseCaseInactiveHistory = new ArrayList<>(); 79 private final List<UseCase> mUseCaseUpdateHistory = new ArrayList<>(); 80 private final List<UseCase> mUseCaseResetHistory = new ArrayList<>(); 81 private boolean mHasTransform = true; 82 private boolean mIsPrimary = true; 83 84 private @Nullable SessionConfig mSessionConfig; 85 86 private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList(); 87 private @Nullable ListenableFuture<List<Surface>> mSessionConfigurationFuture = null; 88 89 private CameraConfig mCameraConfig = CameraConfigs.defaultConfig(); 90 FakeCamera()91 public FakeCamera() { 92 this(DEFAULT_CAMERA_ID, /*cameraControl=*/null, 93 new FakeCameraInfoInternal(DEFAULT_CAMERA_ID)); 94 } 95 FakeCamera(@onNull CameraControlInternal cameraControl)96 public FakeCamera(@NonNull CameraControlInternal cameraControl) { 97 this(DEFAULT_CAMERA_ID, cameraControl, new FakeCameraInfoInternal(DEFAULT_CAMERA_ID)); 98 } 99 FakeCamera(@onNull String cameraId)100 public FakeCamera(@NonNull String cameraId) { 101 this(cameraId, /*cameraControl=*/null, new FakeCameraInfoInternal(cameraId)); 102 } 103 FakeCamera(@ullable CameraControlInternal cameraControl, @NonNull CameraInfoInternal cameraInfo)104 public FakeCamera(@Nullable CameraControlInternal cameraControl, 105 @NonNull CameraInfoInternal cameraInfo) { 106 this(DEFAULT_CAMERA_ID, cameraControl, cameraInfo); 107 } 108 FakeCamera(@onNull String cameraId, @Nullable CameraControlInternal cameraControl, @NonNull CameraInfoInternal cameraInfo)109 public FakeCamera(@NonNull String cameraId, @Nullable CameraControlInternal cameraControl, 110 @NonNull CameraInfoInternal cameraInfo) { 111 mCameraInfoInternal = cameraInfo; 112 mCameraId = cameraId; 113 mUseCaseAttachState = new UseCaseAttachState(cameraId); 114 mCameraControlInternal = cameraControl == null ? new FakeCameraControl( 115 new CameraControlInternal.ControlUpdateCallback() { 116 @Override 117 public void onCameraControlUpdateSessionConfig() { 118 updateCaptureSessionConfig(); 119 } 120 121 @Override 122 public void onCameraControlCaptureRequests( 123 @NonNull List<CaptureConfig> captureConfigs) { 124 Logger.d(TAG, "Capture requests submitted:\n " + TextUtils.join("\n ", 125 captureConfigs)); 126 } 127 }) 128 : cameraControl; 129 setState(State.CLOSED); 130 } 131 132 /** 133 * Sets the number of cameras that are available to open. 134 * 135 * <p>If this number is set to 0, then calling {@link #open()} will wait in a {@code 136 * PENDING_OPEN} state until the number is set to a value greater than 0 before entering an 137 * {@code OPEN} state. 138 * 139 * @param count An integer number greater than 0 representing the number of available cameras 140 * to open on this device. 141 */ setAvailableCameraCount(@ntRangefrom = 0) int count)142 public void setAvailableCameraCount(@IntRange(from = 0) int count) { 143 Preconditions.checkArgumentNonnegative(count); 144 mAvailableCameraCount = count; 145 if (mAvailableCameraCount > 0 && mState == State.PENDING_OPEN) { 146 open(); 147 } 148 } 149 150 /** 151 * Retrieves the number of cameras available to open on this device, as seen by this camera. 152 * 153 * @return An integer number greater than 0 representing the number of available cameras to 154 * open on this device. 155 */ 156 @IntRange(from = 0) getAvailableCameraCount()157 public int getAvailableCameraCount() { 158 return mAvailableCameraCount; 159 } 160 161 @Override open()162 public void open() { 163 checkNotReleased(); 164 if (mState == State.CLOSED || mState == State.PENDING_OPEN) { 165 if (mAvailableCameraCount > 0) { 166 setState(State.OPEN); 167 } else { 168 setState(State.PENDING_OPEN); 169 } 170 } 171 } 172 173 @Override close()174 public void close() { 175 checkNotReleased(); 176 switch (mState) { 177 case OPEN: 178 // fall through 179 case CONFIGURED: 180 mSessionConfig = null; 181 reconfigure(); 182 // fall through 183 case PENDING_OPEN: 184 setState(State.CLOSED); 185 break; 186 default: 187 break; 188 } 189 } 190 191 @Override release()192 public @NonNull ListenableFuture<Void> release() { 193 if (mState == State.OPEN) { 194 close(); 195 } 196 197 if (mState != State.RELEASED) { 198 setState(State.RELEASED); 199 } 200 return Futures.immediateFuture(null); 201 } 202 203 @Override getCameraState()204 public @NonNull Observable<CameraInternal.State> getCameraState() { 205 return mObservableState; 206 } 207 208 @Override onUseCaseActive(@onNull UseCase useCase)209 public void onUseCaseActive(@NonNull UseCase useCase) { 210 Logger.d(TAG, "Use case " + useCase + " ACTIVE for camera " + mCameraId); 211 mUseCaseActiveHistory.add(useCase); 212 mUseCaseAttachState.setUseCaseActive(useCase.getName() + useCase.hashCode(), 213 useCase.getSessionConfig(), useCase.getCurrentConfig(), 214 useCase.getAttachedStreamSpec(), 215 Collections.singletonList(useCase.getCurrentConfig().getCaptureType())); 216 updateCaptureSessionConfig(); 217 } 218 219 /** Removes the use case from a state of issuing capture requests. */ 220 @Override onUseCaseInactive(@onNull UseCase useCase)221 public void onUseCaseInactive(@NonNull UseCase useCase) { 222 Logger.d(TAG, "Use case " + useCase + " INACTIVE for camera " + mCameraId); 223 mUseCaseInactiveHistory.add(useCase); 224 mUseCaseAttachState.setUseCaseInactive(useCase.getName() + useCase.hashCode()); 225 updateCaptureSessionConfig(); 226 } 227 228 /** Updates the capture requests based on the latest settings. */ 229 @Override onUseCaseUpdated(@onNull UseCase useCase)230 public void onUseCaseUpdated(@NonNull UseCase useCase) { 231 Logger.d(TAG, "Use case " + useCase + " UPDATED for camera " + mCameraId); 232 mUseCaseUpdateHistory.add(useCase); 233 mUseCaseAttachState.updateUseCase(useCase.getName() + useCase.hashCode(), 234 useCase.getSessionConfig(), useCase.getCurrentConfig(), 235 useCase.getAttachedStreamSpec(), 236 Collections.singletonList(useCase.getCurrentConfig().getCaptureType())); 237 updateCaptureSessionConfig(); 238 } 239 240 @Override onUseCaseReset(@onNull UseCase useCase)241 public void onUseCaseReset(@NonNull UseCase useCase) { 242 Logger.d(TAG, "Use case " + useCase + " RESET for camera " + mCameraId); 243 mUseCaseResetHistory.add(useCase); 244 mUseCaseAttachState.updateUseCase(useCase.getName() + useCase.hashCode(), 245 useCase.getSessionConfig(), useCase.getCurrentConfig(), 246 useCase.getAttachedStreamSpec(), 247 Collections.singletonList(useCase.getCurrentConfig().getCaptureType())); 248 updateCaptureSessionConfig(); 249 openCaptureSession(); 250 } 251 252 /** 253 * Sets the use cases to be in the state where the capture session will be configured to handle 254 * capture requests from the use case. 255 */ 256 @Override attachUseCases(final @NonNull Collection<UseCase> useCases)257 public void attachUseCases(final @NonNull Collection<UseCase> useCases) { 258 if (useCases.isEmpty()) { 259 return; 260 } 261 262 mAttachedUseCases.addAll(useCases); 263 264 Logger.d(TAG, "Use cases " + useCases + " ATTACHED for camera " + mCameraId); 265 for (UseCase useCase : useCases) { 266 useCase.onStateAttached(); 267 useCase.onCameraControlReady(); 268 mUseCaseAttachState.setUseCaseAttached( 269 useCase.getName() + useCase.hashCode(), 270 useCase.getSessionConfig(), 271 useCase.getCurrentConfig(), 272 useCase.getAttachedStreamSpec(), 273 Collections.singletonList(useCase.getCurrentConfig().getCaptureType())); 274 } 275 276 open(); 277 updateCaptureSessionConfig(); 278 openCaptureSession(); 279 } 280 281 /** 282 * Removes the use cases to be in the state where the capture session will be configured to 283 * handle capture requests from the use case. 284 */ 285 @Override detachUseCases(final @NonNull Collection<UseCase> useCases)286 public void detachUseCases(final @NonNull Collection<UseCase> useCases) { 287 if (useCases.isEmpty()) { 288 return; 289 } 290 291 mAttachedUseCases.removeAll(useCases); 292 293 Logger.d(TAG, "Use cases " + useCases + " DETACHED for camera " + mCameraId); 294 for (UseCase useCase : useCases) { 295 mUseCaseAttachState.setUseCaseDetached(useCase.getName() + useCase.hashCode()); 296 useCase.onStateDetached(); 297 } 298 299 if (mUseCaseAttachState.getAttachedSessionConfigs().isEmpty()) { 300 close(); 301 return; 302 } 303 304 openCaptureSession(); 305 updateCaptureSessionConfig(); 306 } 307 308 /** 309 * Gets the attached use cases. 310 * 311 * @see #attachUseCases 312 * @see #detachUseCases 313 */ getAttachedUseCases()314 public @NonNull Set<UseCase> getAttachedUseCases() { 315 return mAttachedUseCases; 316 } 317 318 // Returns fixed CameraControlInternal instance in order to verify the instance is correctly 319 // attached. 320 @Override getCameraControlInternal()321 public @NonNull CameraControlInternal getCameraControlInternal() { 322 return mCameraControlInternal; 323 } 324 325 @Override getCameraInfoInternal()326 public @NonNull CameraInfoInternal getCameraInfoInternal() { 327 return mCameraInfoInternal; 328 } 329 330 /** 331 * Returns a list of active use cases ordered chronologically according to 332 * {@link #onUseCaseActive} invocations. 333 */ getUseCaseActiveHistory()334 public @NonNull List<UseCase> getUseCaseActiveHistory() { 335 return mUseCaseActiveHistory; 336 } 337 338 /** 339 * Returns a list of inactive use cases ordered chronologically according to 340 * {@link #onUseCaseInactive} invocations. 341 */ getUseCaseInactiveHistory()342 public @NonNull List<UseCase> getUseCaseInactiveHistory() { 343 return mUseCaseInactiveHistory; 344 } 345 346 347 /** 348 * Returns a list of updated use cases ordered chronologically according to 349 * {@link #onUseCaseUpdated} invocations. 350 */ getUseCaseUpdateHistory()351 public @NonNull List<UseCase> getUseCaseUpdateHistory() { 352 return mUseCaseUpdateHistory; 353 } 354 355 356 /** 357 * Returns a list of reset use cases ordered chronologically according to 358 * {@link #onUseCaseReset} invocations. 359 */ getUseCaseResetHistory()360 public @NonNull List<UseCase> getUseCaseResetHistory() { 361 return mUseCaseResetHistory; 362 } 363 364 @Override getHasTransform()365 public boolean getHasTransform() { 366 return mHasTransform; 367 } 368 369 /** 370 * Sets whether the camera has a transform. 371 */ setHasTransform(boolean hasCameraTransform)372 public void setHasTransform(boolean hasCameraTransform) { 373 mHasTransform = hasCameraTransform; 374 } 375 376 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 377 @Override setPrimary(boolean isPrimary)378 public void setPrimary(boolean isPrimary) { 379 mIsPrimary = isPrimary; 380 } 381 382 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) isPrimary()383 public boolean isPrimary() { 384 return mIsPrimary; 385 } 386 checkNotReleased()387 private void checkNotReleased() { 388 if (isReleased()) { 389 throw new IllegalStateException("Camera has been released."); 390 } 391 } 392 openCaptureSession()393 private void openCaptureSession() { 394 SessionConfig.ValidatingBuilder validatingBuilder; 395 validatingBuilder = mUseCaseAttachState.getAttachedBuilder(); 396 if (!validatingBuilder.isValid()) { 397 Logger.d(TAG, "Unable to create capture session due to conflicting configurations"); 398 return; 399 } 400 401 if (mState != State.OPEN) { 402 Logger.d(TAG, "CameraDevice is not opened"); 403 return; 404 } 405 406 mSessionConfig = validatingBuilder.build(); 407 reconfigure(); 408 } 409 410 @SuppressWarnings("WeakerAccess") /* synthetic accessor */ updateCaptureSessionConfig()411 private void updateCaptureSessionConfig() { 412 SessionConfig.ValidatingBuilder validatingBuilder; 413 validatingBuilder = mUseCaseAttachState.getActiveAndAttachedBuilder(); 414 415 if (validatingBuilder.isValid()) { 416 // Apply CameraControlInternal's SessionConfig to let CameraControlInternal be able 417 // to control Repeating Request and process results. 418 validatingBuilder.add(mCameraControlInternal.getSessionConfig()); 419 420 mSessionConfig = validatingBuilder.build(); 421 } 422 } 423 reconfigure()424 private void reconfigure() { 425 notifySurfaceDetached(); 426 427 if (mSessionConfig != null) { 428 List<DeferrableSurface> surfaces = mSessionConfig.getSurfaces(); 429 430 mConfiguredDeferrableSurfaces = new ArrayList<>(surfaces); 431 432 // Since this is a fake camera, it is likely we will get null surfaces. Don't 433 // consider them as failed. 434 mSessionConfigurationFuture = 435 DeferrableSurfaces.surfaceListWithTimeout(mConfiguredDeferrableSurfaces, false, 436 TIMEOUT_GET_SURFACE_IN_MS, CameraXExecutors.directExecutor(), 437 CameraXExecutors.myLooperExecutor()); 438 439 Futures.addCallback(mSessionConfigurationFuture, new FutureCallback<List<Surface>>() { 440 @Override 441 public void onSuccess(@Nullable List<Surface> result) { 442 if (result == null || result.isEmpty()) { 443 Logger.e(TAG, "Unable to open capture session with no surfaces. "); 444 445 if (mState == State.OPEN) { 446 setState(mState, 447 CameraState.StateError.create(CameraState.ERROR_STREAM_CONFIG)); 448 } 449 return; 450 } 451 setState(State.CONFIGURED); 452 } 453 454 @Override 455 public void onFailure(@NonNull Throwable t) { 456 if (mState == State.OPEN) { 457 setState(mState, 458 CameraState.StateError.create(CameraState.ERROR_STREAM_CONFIG, t)); 459 } 460 } 461 }, CameraXExecutors.directExecutor()); 462 } 463 464 notifySurfaceAttached(); 465 } 466 467 // Notify the surface is attached to a new capture session. notifySurfaceAttached()468 private void notifySurfaceAttached() { 469 for (DeferrableSurface deferrableSurface : mConfiguredDeferrableSurfaces) { 470 try { 471 deferrableSurface.incrementUseCount(); 472 } catch (DeferrableSurface.SurfaceClosedException e) { 473 throw new RuntimeException("Surface in unexpected state", e); 474 } 475 } 476 } 477 478 // Notify the surface is detached from current capture session. notifySurfaceDetached()479 private void notifySurfaceDetached() { 480 for (DeferrableSurface deferredSurface : mConfiguredDeferrableSurfaces) { 481 deferredSurface.decrementUseCount(); 482 } 483 // Clears the mConfiguredDeferrableSurfaces to prevent from duplicate 484 // notifySurfaceDetached calls. 485 mConfiguredDeferrableSurfaces.clear(); 486 } 487 488 @Override getExtendedConfig()489 public @NonNull CameraConfig getExtendedConfig() { 490 return mCameraConfig; 491 } 492 493 @Override setExtendedConfig(@ullable CameraConfig cameraConfig)494 public void setExtendedConfig(@Nullable CameraConfig cameraConfig) { 495 mCameraConfig = cameraConfig; 496 } 497 498 /** Returns whether camera is already released. */ 499 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) isReleased()500 public boolean isReleased() { 501 return mState == State.RELEASED; 502 } 503 setState(CameraInternal.State state)504 private void setState(CameraInternal.State state) { 505 setState(state, null); 506 } 507 setState(CameraInternal.State state, CameraState.StateError stateError)508 private void setState(CameraInternal.State state, CameraState.StateError stateError) { 509 mState = state; 510 mObservableState.postValue(state); 511 if (mCameraInfoInternal instanceof FakeCameraInfoInternal) { 512 ((FakeCameraInfoInternal) mCameraInfoInternal).updateCameraState( 513 CameraState.create(getCameraStateType(state), stateError)); 514 } 515 } 516 getCameraStateType(CameraInternal.State state)517 private CameraState.Type getCameraStateType(CameraInternal.State state) { 518 switch (state) { 519 case PENDING_OPEN: 520 return CameraState.Type.PENDING_OPEN; 521 case OPENING: 522 return CameraState.Type.OPENING; 523 case OPEN: 524 case CONFIGURED: 525 return CameraState.Type.OPEN; 526 case CLOSING: 527 case RELEASING: 528 return CameraState.Type.CLOSING; 529 case CLOSED: 530 case RELEASED: 531 return CameraState.Type.CLOSED; 532 default: 533 throw new IllegalStateException( 534 "Unknown internal camera state: " + state); 535 } 536 } 537 538 /** 539 * Waits for session configuration to be completed. 540 * 541 * @param timeoutMillis The waiting timeout in milliseconds. 542 */ 543 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) awaitSessionConfiguration(long timeoutMillis)544 public void awaitSessionConfiguration(long timeoutMillis) { 545 if (mSessionConfigurationFuture == null) { 546 Logger.e(TAG, "mSessionConfigurationFuture is null!"); 547 return; 548 } 549 550 try { 551 mSessionConfigurationFuture.get(timeoutMillis, TimeUnit.MILLISECONDS); 552 } catch (ExecutionException | InterruptedException | TimeoutException e) { 553 Logger.e(TAG, "Session configuration did not complete within " + timeoutMillis + " ms", 554 e); 555 } 556 } 557 558 /** 559 * Simulates a capture frame being drawn on the session config surfaces to imitate a real 560 * camera. 561 */ 562 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) simulateCaptureFrameAsync()563 public @NonNull ListenableFuture<Void> simulateCaptureFrameAsync() { 564 return simulateCaptureFrameAsync(null); 565 } 566 567 /** 568 * Simulates a capture frame being drawn on the session config surfaces to imitate a real 569 * camera. 570 * 571 * <p> This method uses the provided {@link Executor} for the asynchronous operations in case 572 * of specific thread requirements. 573 */ 574 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) simulateCaptureFrameAsync(@ullable Executor executor)575 public @NonNull ListenableFuture<Void> simulateCaptureFrameAsync(@Nullable Executor executor) { 576 // Since capture session is not configured synchronously and may be dependent on when a 577 // surface can be obtained from DeferrableSurface, we should wait for the session 578 // configuration here just-in-case. 579 awaitSessionConfiguration(1000); 580 581 if (mSessionConfig == null || mState != State.CONFIGURED) { 582 return Futures.immediateFailedFuture( 583 new IllegalStateException("Session config not successfully configured yet.")); 584 } 585 586 if (executor == null) { 587 return CaptureSimulationKt.simulateCaptureFrameAsync(mSessionConfig.getSurfaces()); 588 } 589 return CaptureSimulationKt.simulateCaptureFrameAsync(mSessionConfig.getSurfaces(), 590 executor); 591 } 592 } 593