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