1 /*
<lambda>null2  * Copyright 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.lifecycle
18 
19 import android.content.Context
20 import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
21 import android.graphics.Rect
22 import android.os.Handler
23 import android.os.Looper
24 import android.util.Rational
25 import android.view.Surface
26 import androidx.annotation.OptIn
27 import androidx.annotation.RequiresApi
28 import androidx.camera.camera2.Camera2Config
29 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
30 import androidx.camera.core.CameraFilter
31 import androidx.camera.core.CameraInfo
32 import androidx.camera.core.CameraSelector
33 import androidx.camera.core.CameraSelector.LENS_FACING_BACK
34 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
35 import androidx.camera.core.CameraXConfig
36 import androidx.camera.core.ConcurrentCamera.SingleCameraConfig
37 import androidx.camera.core.ImageAnalysis
38 import androidx.camera.core.ImageCapture
39 import androidx.camera.core.Preview
40 import androidx.camera.core.UseCase
41 import androidx.camera.core.UseCaseGroup
42 import androidx.camera.core.ViewPort
43 import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED
44 import androidx.camera.core.impl.AdapterCameraInfo
45 import androidx.camera.core.impl.CameraConfig
46 import androidx.camera.core.impl.CameraFactory
47 import androidx.camera.core.impl.CameraInfoInternal
48 import androidx.camera.core.impl.CameraThreadConfig
49 import androidx.camera.core.impl.Config
50 import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
51 import androidx.camera.core.impl.Identifier
52 import androidx.camera.core.impl.MutableOptionsBundle
53 import androidx.camera.core.impl.SessionProcessor
54 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
55 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
56 import androidx.camera.core.internal.utils.ImageUtil
57 import androidx.camera.testing.fakes.FakeAppConfig
58 import androidx.camera.testing.fakes.FakeCamera
59 import androidx.camera.testing.fakes.FakeCameraInfoInternal
60 import androidx.camera.testing.impl.CameraPipeConfigTestRule
61 import androidx.camera.testing.impl.CameraUtil
62 import androidx.camera.testing.impl.fakes.FakeCameraConfig
63 import androidx.camera.testing.impl.fakes.FakeCameraCoordinator
64 import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager
65 import androidx.camera.testing.impl.fakes.FakeCameraFactory
66 import androidx.camera.testing.impl.fakes.FakeCameraFilter
67 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
68 import androidx.camera.testing.impl.fakes.FakeSessionProcessor
69 import androidx.camera.testing.impl.fakes.FakeSurfaceEffect
70 import androidx.camera.testing.impl.fakes.FakeSurfaceProcessor
71 import androidx.camera.testing.impl.fakes.FakeUseCase
72 import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
73 import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory
74 import androidx.camera.video.Recorder
75 import androidx.camera.video.VideoCapture
76 import androidx.concurrent.futures.await
77 import androidx.test.core.app.ApplicationProvider
78 import androidx.test.filters.SdkSuppress
79 import androidx.test.filters.SmallTest
80 import androidx.testutils.assertThrows
81 import com.google.common.truth.Truth.assertThat
82 import kotlinx.coroutines.Dispatchers
83 import kotlinx.coroutines.MainScope
84 import kotlinx.coroutines.runBlocking
85 import org.junit.After
86 import org.junit.Assume.assumeTrue
87 import org.junit.Before
88 import org.junit.Rule
89 import org.junit.Test
90 import org.junit.runner.RunWith
91 import org.junit.runners.Parameterized
92 
93 @SmallTest
94 @RunWith(Parameterized::class)
95 @SdkSuppress(minSdkVersion = 21)
96 class ProcessCameraProviderTest(
97     private val implName: String,
98     private val cameraConfig: CameraXConfig,
99 ) {
100 
101     @get:Rule
102     val cameraPipeConfigTestRule =
103         CameraPipeConfigTestRule(
104             active = implName.contains(CameraPipeConfig::class.simpleName!!),
105         )
106 
107     @get:Rule
108     val cameraRule =
109         CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
110             CameraUtil.PreTestCameraIdList(cameraConfig)
111         )
112 
113     companion object {
114         @JvmStatic
115         @Parameterized.Parameters(name = "{0}")
116         fun data() =
117             listOf(
118                 arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
119                 arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
120             )
121     }
122 
123     private val context = ApplicationProvider.getApplicationContext() as Context
124     private val lifecycleOwner0 = FakeLifecycleOwner()
125     private val lifecycleOwner1 = FakeLifecycleOwner()
126     private val cameraCoordinator = FakeCameraCoordinator()
127     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
128 
129     private lateinit var provider: ProcessCameraProvider
130 
131     @Before
132     fun setUp() {
133         assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
134     }
135 
136     @After
137     fun tearDown() {
138         runBlocking(MainScope().coroutineContext) {
139             try {
140                 val provider = ProcessCameraProvider.getInstance(context).await()
141                 provider.shutdownAsync().await()
142             } catch (e: IllegalStateException) {
143                 // ProcessCameraProvider may not be configured. Ignore.
144             }
145         }
146     }
147 
148     @Test
149     fun bindUseCaseGroupWithEffect_effectIsSetOnUseCase() {
150         // Arrange.
151         ProcessCameraProvider.configureInstance(cameraConfig)
152         val surfaceProcessor = FakeSurfaceProcessor(mainThreadExecutor())
153         val effect = FakeSurfaceEffect(mainThreadExecutor(), surfaceProcessor)
154         val preview = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
155         val useCaseGroup = UseCaseGroup.Builder().addUseCase(preview).addEffect(effect).build()
156 
157         runBlocking(MainScope().coroutineContext) {
158             // Act.
159             provider = ProcessCameraProvider.getInstance(context).await()
160             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCaseGroup)
161 
162             // Assert.
163             assertThat(preview.effect).isEqualTo(effect)
164             assertThat(provider.isConcurrentCameraModeOn).isFalse()
165         }
166     }
167 
168     @OptIn(ExperimentalCameraProviderConfiguration::class)
169     @Test
170     fun canRetrieveCamera_withZeroUseCases() {
171         ProcessCameraProvider.configureInstance(cameraConfig)
172         runBlocking(MainScope().coroutineContext) {
173             provider = ProcessCameraProvider.getInstance(context).await()
174             val camera = provider.bindToLifecycle(lifecycleOwner0, cameraSelector)
175             assertThat(camera).isNotNull()
176             assertThat(provider.isConcurrentCameraModeOn).isFalse()
177         }
178     }
179 
180     @Test
181     fun bindUseCase_isBound() {
182         ProcessCameraProvider.configureInstance(cameraConfig)
183 
184         runBlocking(MainScope().coroutineContext) {
185             provider = ProcessCameraProvider.getInstance(context).await()
186             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
187 
188             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase)
189 
190             assertThat(provider.isBound(useCase)).isTrue()
191             assertThat(provider.isConcurrentCameraModeOn).isFalse()
192         }
193     }
194 
195     @Test
196     fun bindSecondUseCaseToDifferentLifecycle_firstUseCaseStillBound() {
197         ProcessCameraProvider.configureInstance(cameraConfig)
198 
199         runBlocking(MainScope().coroutineContext) {
200             provider = ProcessCameraProvider.getInstance(context).await()
201 
202             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
203             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
204 
205             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0)
206             provider.bindToLifecycle(lifecycleOwner1, cameraSelector, useCase1)
207 
208             // TODO(b/158595693) Add check on whether or not camera for fakeUseCase0 should be
209             //  exist or not
210             // assertThat(fakeUseCase0.camera).isNotNull() (or isNull()?)
211             assertThat(provider.isBound(useCase0)).isTrue()
212             assertThat(useCase1.camera).isNotNull()
213             assertThat(provider.isBound(useCase1)).isTrue()
214             assertThat(provider.isConcurrentCameraModeOn).isFalse()
215         }
216     }
217 
218     @Test
219     fun isNotBound_afterUnbind() {
220         ProcessCameraProvider.configureInstance(cameraConfig)
221 
222         runBlocking(MainScope().coroutineContext) {
223             provider = ProcessCameraProvider.getInstance(context).await()
224 
225             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
226 
227             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase)
228 
229             provider.unbind(useCase)
230 
231             assertThat(provider.isBound(useCase)).isFalse()
232             assertThat(provider.isConcurrentCameraModeOn).isFalse()
233         }
234     }
235 
236     @Test
237     fun unbindFirstUseCase_secondUseCaseStillBound() {
238         ProcessCameraProvider.configureInstance(cameraConfig)
239 
240         runBlocking(MainScope().coroutineContext) {
241             provider = ProcessCameraProvider.getInstance(context).await()
242 
243             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
244             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
245 
246             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0, useCase1)
247 
248             provider.unbind(useCase0)
249 
250             assertThat(useCase0.camera).isNull()
251             assertThat(provider.isBound(useCase0)).isFalse()
252             assertThat(useCase1.camera).isNotNull()
253             assertThat(provider.isBound(useCase1)).isTrue()
254             assertThat(provider.isConcurrentCameraModeOn).isFalse()
255         }
256     }
257 
258     @Test
259     fun unbindAll_unbindsAllUseCasesFromCameras() {
260         ProcessCameraProvider.configureInstance(cameraConfig)
261 
262         runBlocking(MainScope().coroutineContext) {
263             provider = ProcessCameraProvider.getInstance(context).await()
264 
265             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
266 
267             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase)
268 
269             provider.unbindAll()
270 
271             assertThat(useCase.camera).isNull()
272             assertThat(provider.isBound(useCase)).isFalse()
273             assertThat(provider.isConcurrentCameraModeOn).isFalse()
274         }
275     }
276 
277     @Test
278     fun bindMultipleUseCases() {
279         ProcessCameraProvider.configureInstance(cameraConfig)
280 
281         runBlocking(MainScope().coroutineContext) {
282             provider = ProcessCameraProvider.getInstance(context).await()
283 
284             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
285             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
286 
287             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0, useCase1)
288 
289             assertThat(provider.isBound(useCase0)).isTrue()
290             assertThat(provider.isBound(useCase1)).isTrue()
291             assertThat(provider.isConcurrentCameraModeOn).isFalse()
292         }
293     }
294 
295     @Test
296     fun bind_createsDifferentLifecycleCameras_forDifferentLifecycles() {
297         ProcessCameraProvider.configureInstance(cameraConfig)
298 
299         runBlocking(MainScope().coroutineContext) {
300             provider = ProcessCameraProvider.getInstance(context).await()
301 
302             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
303             val camera0 = provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0)
304 
305             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
306             val camera1 = provider.bindToLifecycle(lifecycleOwner1, cameraSelector, useCase1)
307 
308             assertThat(camera0).isNotEqualTo(camera1)
309             assertThat(provider.isConcurrentCameraModeOn).isFalse()
310         }
311     }
312 
313     @Test
314     fun bind_returnTheSameCameraForSameSelectorAndLifecycleOwner() {
315         ProcessCameraProvider.configureInstance(cameraConfig)
316 
317         runBlocking(MainScope().coroutineContext) {
318             provider = ProcessCameraProvider.getInstance(context).await()
319             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
320             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
321 
322             val camera0 = provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0)
323             val camera1 = provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase1)
324 
325             assertThat(camera0).isSameInstanceAs(camera1)
326             assertThat(provider.isConcurrentCameraModeOn).isFalse()
327         }
328     }
329 
330     @Test
331     fun bindUseCases_withDifferentLensFacingButSameLifecycleOwner() {
332         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
333         ProcessCameraProvider.configureInstance(cameraConfig)
334 
335         runBlocking(MainScope().coroutineContext) {
336             provider = ProcessCameraProvider.getInstance(context).await()
337 
338             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
339             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
340 
341             provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0)
342 
343             assertThrows<IllegalArgumentException> {
344                 provider.bindToLifecycle(
345                     lifecycleOwner0,
346                     CameraSelector.DEFAULT_FRONT_CAMERA,
347                     useCase1
348                 )
349             }
350             assertThat(provider.isConcurrentCameraModeOn).isFalse()
351         }
352     }
353 
354     @Test
355     fun bindUseCases_withDifferentLensFacingAndLifecycle() {
356         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
357         ProcessCameraProvider.configureInstance(cameraConfig)
358 
359         runBlocking(MainScope().coroutineContext) {
360             provider = ProcessCameraProvider.getInstance(context).await()
361 
362             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
363             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
364 
365             val camera0 = provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase0)
366 
367             val camera1 =
368                 provider.bindToLifecycle(
369                     lifecycleOwner1,
370                     CameraSelector.DEFAULT_FRONT_CAMERA,
371                     useCase1
372                 )
373 
374             assertThat(camera0).isNotEqualTo(camera1)
375             assertThat(provider.isConcurrentCameraModeOn).isFalse()
376         }
377     }
378 
379     @Test
380     fun bindUseCases_withNotExistedLensFacingCamera() {
381         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
382         val cameraFactoryProvider =
383             CameraFactory.Provider { _, _, _, _ ->
384                 val cameraFactory = FakeCameraFactory()
385                 cameraFactory.insertCamera(LENS_FACING_BACK, "0") {
386                     FakeCamera("0", null, FakeCameraInfoInternal("0", 0, LENS_FACING_BACK))
387                 }
388                 cameraFactory.cameraCoordinator = FakeCameraCoordinator()
389                 cameraFactory
390             }
391 
392         val appConfigBuilder =
393             CameraXConfig.Builder()
394                 .setCameraFactoryProvider(cameraFactoryProvider)
395                 .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
396                 .setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
397 
398         ProcessCameraProvider.configureInstance(appConfigBuilder.build())
399 
400         runBlocking(MainScope().coroutineContext) {
401             provider = ProcessCameraProvider.getInstance(context).await()
402 
403             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
404 
405             // The front camera is not defined, we should get the IllegalArgumentException when it
406             // tries to get the camera.
407             assertThrows<IllegalArgumentException> {
408                 provider.bindToLifecycle(
409                     lifecycleOwner0,
410                     CameraSelector.DEFAULT_FRONT_CAMERA,
411                     useCase
412                 )
413             }
414             assertThat(provider.isConcurrentCameraModeOn).isFalse()
415         }
416     }
417 
418     @Test
419     fun bindUseCases_viewPortUpdated() {
420         runBlocking(MainScope().coroutineContext) {
421             // Arrange.
422             ProcessCameraProvider.configureInstance(cameraConfig)
423             provider = ProcessCameraProvider.awaitInstance(context)
424             val preview = Preview.Builder().build()
425             val imageCapture = ImageCapture.Builder().build()
426             val imageAnalysis = ImageAnalysis.Builder().build()
427             val videoCapture = VideoCapture.Builder(Recorder.Builder().build()).build()
428             val aspectRatio = Rational(2, 1)
429             val viewPort = ViewPort.Builder(aspectRatio, Surface.ROTATION_0).build()
430 
431             // Act.
432             provider.bindToLifecycle(
433                 FakeLifecycleOwner(),
434                 cameraSelector,
435                 UseCaseGroup.Builder()
436                     .setViewPort(viewPort)
437                     .addUseCase(preview)
438                     .addUseCase(imageCapture)
439                     .addUseCase(imageAnalysis)
440                     .addUseCase(videoCapture)
441                     .build()
442             )
443 
444             // Assert: The aspect ratio of the use cases should be close to the aspect ratio of the
445             // view port set to the UseCaseGroup.
446             val aspectRatioThreshold = 0.01
447             assertThat(preview.viewPortCropRect!!.aspectRatio().toDouble())
448                 .isWithin(aspectRatioThreshold)
449                 .of(preview.getExpectedAspectRatio(aspectRatio))
450             assertThat(imageCapture.viewPortCropRect!!.aspectRatio().toDouble())
451                 .isWithin(aspectRatioThreshold)
452                 .of(imageCapture.getExpectedAspectRatio(aspectRatio))
453             assertThat(imageAnalysis.viewPortCropRect!!.aspectRatio().toDouble())
454                 .isWithin(aspectRatioThreshold)
455                 .of(imageAnalysis.getExpectedAspectRatio(aspectRatio))
456             assertThat(videoCapture.viewPortCropRect!!.aspectRatio().toDouble())
457                 .isWithin(aspectRatioThreshold)
458                 .of(videoCapture.getExpectedAspectRatio(aspectRatio))
459         }
460     }
461 
462     private fun UseCase.getExpectedAspectRatio(aspectRatio: Rational): Double {
463         val camera = this.camera!!
464         val isStreamSharingOn = !camera.hasTransform
465         // If stream sharing is on, the expected aspect ratio doesn't have to be adjusted with
466         // sensor rotation.
467         val rotation = if (isStreamSharingOn) 0 else camera.cameraInfo.sensorRotationDegrees
468         return ImageUtil.getRotatedAspectRatio(rotation, aspectRatio).toDouble()
469     }
470 
471     @Test
472     fun lifecycleCameraIsNotActive_withZeroUseCases_bindBeforeLifecycleStarted() {
473         ProcessCameraProvider.configureInstance(cameraConfig)
474         runBlocking(MainScope().coroutineContext) {
475             provider = ProcessCameraProvider.getInstance(context).await()
476             val camera: LifecycleCamera =
477                 provider.bindToLifecycle(lifecycleOwner0, cameraSelector) as LifecycleCamera
478             lifecycleOwner0.startAndResume()
479             assertThat(camera.isActive).isFalse()
480             assertThat(provider.isConcurrentCameraModeOn).isFalse()
481         }
482     }
483 
484     @Test
485     fun lifecycleCameraIsNotActive_withZeroUseCases_bindAfterLifecycleStarted() {
486         ProcessCameraProvider.configureInstance(cameraConfig)
487         runBlocking(MainScope().coroutineContext) {
488             provider = ProcessCameraProvider.getInstance(context).await()
489             lifecycleOwner0.startAndResume()
490             val camera: LifecycleCamera =
491                 provider.bindToLifecycle(lifecycleOwner0, cameraSelector) as LifecycleCamera
492             assertThat(camera.isActive).isFalse()
493             assertThat(provider.isConcurrentCameraModeOn).isFalse()
494         }
495     }
496 
497     @Test
498     fun lifecycleCameraIsActive_withUseCases_bindBeforeLifecycleStarted() {
499         ProcessCameraProvider.configureInstance(cameraConfig)
500         runBlocking(MainScope().coroutineContext) {
501             provider = ProcessCameraProvider.getInstance(context).await()
502             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
503             val camera: LifecycleCamera =
504                 provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase)
505                     as LifecycleCamera
506             lifecycleOwner0.startAndResume()
507             assertThat(camera.isActive).isTrue()
508             assertThat(provider.isConcurrentCameraModeOn).isFalse()
509         }
510     }
511 
512     @Test
513     fun lifecycleCameraIsActive_withUseCases_bindAfterLifecycleStarted() {
514         ProcessCameraProvider.configureInstance(cameraConfig)
515         runBlocking(MainScope().coroutineContext) {
516             provider = ProcessCameraProvider.getInstance(context).await()
517             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
518             lifecycleOwner0.startAndResume()
519             val camera: LifecycleCamera =
520                 provider.bindToLifecycle(lifecycleOwner0, cameraSelector, useCase)
521                     as LifecycleCamera
522             assertThat(camera.isActive).isTrue()
523             assertThat(provider.isConcurrentCameraModeOn).isFalse()
524         }
525     }
526 
527     @Test
528     fun lifecycleCameraIsNotActive_bindAfterLifecycleDestroyed() {
529         ProcessCameraProvider.configureInstance(cameraConfig)
530         runBlocking(MainScope().coroutineContext) {
531             provider = ProcessCameraProvider.getInstance(context).await()
532             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
533             lifecycleOwner0.destroy()
534             val camera: LifecycleCamera =
535                 provider.bindToLifecycle(
536                     lifecycleOwner0,
537                     CameraSelector.DEFAULT_BACK_CAMERA,
538                     useCase
539                 ) as LifecycleCamera
540             assertThat(camera.isActive).isFalse()
541         }
542     }
543 
544     @Test
545     fun lifecycleCameraIsNotActive_unbindUseCase() {
546         ProcessCameraProvider.configureInstance(cameraConfig)
547         runBlocking(MainScope().coroutineContext) {
548             provider = ProcessCameraProvider.getInstance(context).await()
549             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
550             lifecycleOwner0.startAndResume()
551             val camera: LifecycleCamera =
552                 provider.bindToLifecycle(
553                     lifecycleOwner0,
554                     CameraSelector.DEFAULT_BACK_CAMERA,
555                     useCase
556                 ) as LifecycleCamera
557             assertThat(camera.isActive).isTrue()
558             provider.unbind(useCase)
559             assertThat(camera.isActive).isFalse()
560             assertThat(provider.isConcurrentCameraModeOn).isFalse()
561         }
562     }
563 
564     @Test
565     fun lifecycleCameraIsNotActive_unbindAll() {
566         ProcessCameraProvider.configureInstance(cameraConfig)
567         runBlocking(MainScope().coroutineContext) {
568             provider = ProcessCameraProvider.getInstance(context).await()
569             val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
570             lifecycleOwner0.startAndResume()
571             val camera: LifecycleCamera =
572                 provider.bindToLifecycle(
573                     lifecycleOwner0,
574                     CameraSelector.DEFAULT_BACK_CAMERA,
575                     useCase
576                 ) as LifecycleCamera
577             assertThat(camera.isActive).isTrue()
578             provider.unbindAll()
579             assertThat(camera.isActive).isFalse()
580             assertThat(provider.isConcurrentCameraModeOn).isFalse()
581         }
582     }
583 
584     @Test
585     fun getAvailableCameraInfos_usesAllCameras() {
586         ProcessCameraProvider.configureInstance(cameraConfig)
587         runBlocking {
588             provider = ProcessCameraProvider.getInstance(context).await()
589             val cameraCount =
590                 cameraConfig
591                     .getCameraFactoryProvider(null)!!
592                     .newInstance(
593                         context,
594                         CameraThreadConfig.create(
595                             mainThreadExecutor(),
596                             Handler(Looper.getMainLooper())
597                         ),
598                         null,
599                         -1L
600                     )
601                     .availableCameraIds
602                     .size
603 
604             assertThat(provider.availableCameraInfos.size).isEqualTo(cameraCount)
605         }
606     }
607 
608     @Test
609     fun getAvailableCameraInfos_usesFilteredCameras() {
610         ProcessCameraProvider.configureInstance(
611             FakeAppConfig.create(CameraSelector.DEFAULT_BACK_CAMERA)
612         )
613         runBlocking {
614             provider = ProcessCameraProvider.getInstance(context).await()
615 
616             val cameraInfos = provider.availableCameraInfos
617             assertThat(cameraInfos.size).isEqualTo(1)
618 
619             val cameraInfo = cameraInfos.first() as FakeCameraInfoInternal
620             assertThat(cameraInfo.lensFacing).isEqualTo(LENS_FACING_BACK)
621         }
622     }
623 
624     @Test
625     fun getCameraInfo_sameCameraInfoWithBindToLifecycle_afterBinding() {
626         // Arrange.
627         ProcessCameraProvider.configureInstance(cameraConfig)
628 
629         runBlocking(MainScope().coroutineContext) {
630             provider = ProcessCameraProvider.getInstance(context).await()
631             val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
632 
633             // Act: getting the camera info after bindToLifecycle.
634             val camera = provider.bindToLifecycle(lifecycleOwner0, cameraSelector)
635             val cameraInfoInternal1: CameraInfoInternal =
636                 provider.getCameraInfo(cameraSelector) as CameraInfoInternal
637             val cameraInfoInternal2: CameraInfoInternal = camera.cameraInfo as CameraInfoInternal
638 
639             // Assert.
640             assertThat(cameraInfoInternal1).isSameInstanceAs(cameraInfoInternal2)
641         }
642     }
643 
644     @Test
645     fun getCameraInfo_sameCameraInfoWithBindToLifecycle_beforeBinding() {
646         // Arrange.
647         ProcessCameraProvider.configureInstance(cameraConfig)
648         runBlocking(MainScope().coroutineContext) {
649             provider = ProcessCameraProvider.getInstance(context).await()
650             val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
651 
652             // Act: getting the camera info before bindToLifecycle.
653             val cameraInfoInternal1: CameraInfoInternal =
654                 provider.getCameraInfo(cameraSelector) as CameraInfoInternal
655             val camera = provider.bindToLifecycle(lifecycleOwner0, cameraSelector)
656             val cameraInfoInternal2: CameraInfoInternal = camera.cameraInfo as CameraInfoInternal
657 
658             // Assert.
659             assertThat(cameraInfoInternal1).isSameInstanceAs(cameraInfoInternal2)
660         }
661     }
662 
663     @Test
664     fun getCameraInfo_containExtendedCameraConfig() {
665         // Arrange.
666         ProcessCameraProvider.configureInstance(cameraConfig)
667         runBlocking {
668             provider = ProcessCameraProvider.getInstance(context).await()
669             val id = Identifier.create("FakeId")
670             val cameraConfig = FakeCameraConfig(postviewSupported = true)
671             ExtendedCameraConfigProviderStore.addConfig(id) { _, _ -> cameraConfig }
672             val cameraSelector =
673                 CameraSelector.Builder().addCameraFilter(FakeCameraFilter(id)).build()
674 
675             // Act.
676             val adapterCameraInfo = provider.getCameraInfo(cameraSelector) as AdapterCameraInfo
677 
678             // Assert.
679             assertThat(adapterCameraInfo.isPostviewSupported).isTrue()
680         }
681     }
682 
683     @Test
684     fun getCameraInfo_exceptionWhenCameraSelectorInvalid() {
685         // Arrange.
686         ProcessCameraProvider.configureInstance(cameraConfig)
687         runBlocking(MainScope().coroutineContext) {
688             provider = ProcessCameraProvider.getInstance(context).await()
689             // Intentionally create a camera selector that doesn't result in a camera.
690             val cameraSelector =
691                 CameraSelector.Builder().addCameraFilter { ArrayList<CameraInfo>() }.build()
692 
693             // Act & Assert.
694             assertThrows(IllegalArgumentException::class.java) {
695                 provider.getCameraInfo(cameraSelector)
696             }
697         }
698     }
699 
700     @Test
701     fun getAvailableConcurrentCameraInfos() {
702         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
703         runBlocking {
704             provider = ProcessCameraProvider.getInstance(context).await()
705             assertThat(provider.availableConcurrentCameraInfos.size).isEqualTo(2)
706             assertThat(provider.availableConcurrentCameraInfos[0].size).isEqualTo(2)
707             assertThat(provider.availableConcurrentCameraInfos[1].size).isEqualTo(2)
708         }
709     }
710 
711     @Test
712     fun shutdown_clearsPreviousConfiguration() {
713         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
714 
715         runBlocking {
716             provider = ProcessCameraProvider.getInstance(context).await()
717             // Clear the configuration so we can reinit
718             provider.shutdownAsync().await()
719         }
720 
721         // Should not throw exception
722         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
723         assertThat(cameraCoordinator.cameraOperatingMode)
724             .isEqualTo(CAMERA_OPERATING_MODE_UNSPECIFIED)
725         assertThat(cameraCoordinator.concurrentCameraSelectors).isEmpty()
726         assertThat(cameraCoordinator.activeConcurrentCameraInfos).isEmpty()
727     }
728 
729     @Test
730     fun bindConcurrentCamera_isBound() {
731         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
732         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
733 
734         runBlocking(MainScope().coroutineContext) {
735             provider = ProcessCameraProvider.getInstance(context).await()
736             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
737             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
738 
739             val singleCameraConfig0 =
740                 SingleCameraConfig(
741                     CameraSelector.DEFAULT_BACK_CAMERA,
742                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
743                     lifecycleOwner0
744                 )
745             val singleCameraConfig1 =
746                 SingleCameraConfig(
747                     CameraSelector.DEFAULT_FRONT_CAMERA,
748                     UseCaseGroup.Builder().addUseCase(useCase1).build(),
749                     lifecycleOwner1
750                 )
751 
752             if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
753                 val concurrentCamera =
754                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
755 
756                 assertThat(concurrentCamera).isNotNull()
757                 assertThat(concurrentCamera.cameras.size).isEqualTo(2)
758                 assertThat(provider.isBound(useCase0)).isTrue()
759                 assertThat(provider.isBound(useCase1)).isTrue()
760                 assertThat(provider.isConcurrentCameraModeOn).isTrue()
761             } else {
762                 assertThrows<UnsupportedOperationException> {
763                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
764                 }
765             }
766         }
767     }
768 
769     @Test
770     fun bindConcurrentPhysicalCamera_isBound() {
771         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
772         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
773 
774         runBlocking(MainScope().coroutineContext) {
775             provider = ProcessCameraProvider.getInstance(context).await()
776             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
777             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
778 
779             val singleCameraConfig0 =
780                 SingleCameraConfig(
781                     CameraSelector.Builder()
782                         .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
783                         .build(),
784                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
785                     lifecycleOwner0
786                 )
787             val singleCameraConfig1 =
788                 SingleCameraConfig(
789                     CameraSelector.Builder()
790                         .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
791                         .build(),
792                     UseCaseGroup.Builder().addUseCase(useCase1).build(),
793                     lifecycleOwner0
794                 )
795 
796             val concurrentCamera =
797                 provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
798 
799             assertThat(concurrentCamera).isNotNull()
800             assertThat(concurrentCamera.cameras.size).isEqualTo(1)
801             assertThat(provider.isBound(useCase0)).isTrue()
802             assertThat(provider.isBound(useCase1)).isTrue()
803             assertThat(provider.isConcurrentCameraModeOn).isFalse()
804         }
805     }
806 
807     @Test
808     fun bindConcurrentCameraTwice_isBound() {
809         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
810         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
811 
812         runBlocking(MainScope().coroutineContext) {
813             provider = ProcessCameraProvider.getInstance(context).await()
814             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
815             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
816             val useCase2 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
817 
818             val singleCameraConfig0 =
819                 SingleCameraConfig(
820                     CameraSelector.DEFAULT_BACK_CAMERA,
821                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
822                     lifecycleOwner0
823                 )
824             val singleCameraConfig1 =
825                 SingleCameraConfig(
826                     CameraSelector.DEFAULT_FRONT_CAMERA,
827                     UseCaseGroup.Builder().addUseCase(useCase1).build(),
828                     lifecycleOwner1
829                 )
830             val singleCameraConfig2 =
831                 SingleCameraConfig(
832                     CameraSelector.DEFAULT_FRONT_CAMERA,
833                     UseCaseGroup.Builder().addUseCase(useCase2).build(),
834                     lifecycleOwner1
835                 )
836 
837             if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
838                 val concurrentCamera0 =
839                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
840 
841                 assertThat(concurrentCamera0).isNotNull()
842                 assertThat(concurrentCamera0.cameras.size).isEqualTo(2)
843                 assertThat(provider.isBound(useCase0)).isTrue()
844                 assertThat(provider.isBound(useCase1)).isTrue()
845                 assertThat(provider.isConcurrentCameraModeOn).isTrue()
846             } else {
847                 assertThrows<UnsupportedOperationException> {
848                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
849                 }
850             }
851 
852             if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
853                 val concurrentCamera1 =
854                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig2))
855 
856                 assertThat(concurrentCamera1).isNotNull()
857                 assertThat(concurrentCamera1.cameras.size).isEqualTo(2)
858                 assertThat(provider.isBound(useCase0)).isTrue()
859                 assertThat(provider.isBound(useCase2)).isTrue()
860                 assertThat(provider.isConcurrentCameraModeOn).isTrue()
861             } else {
862                 assertThrows<UnsupportedOperationException> {
863                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig2))
864                 }
865             }
866         }
867     }
868 
869     @Test
870     fun bindConcurrentCamera_lessThanTwoSingleCameraConfigs() {
871         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
872 
873         runBlocking(MainScope().coroutineContext) {
874             provider = ProcessCameraProvider.getInstance(context).await()
875             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
876 
877             val singleCameraConfig0 =
878                 SingleCameraConfig(
879                     CameraSelector.DEFAULT_BACK_CAMERA,
880                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
881                     lifecycleOwner0
882                 )
883 
884             assertThrows<IllegalArgumentException> {
885                 provider.bindToLifecycle(listOf(singleCameraConfig0))
886             }
887         }
888     }
889 
890     @Test
891     fun bindConcurrentCamera_moreThanTwoSingleCameraConfigs() {
892         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
893         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
894 
895         runBlocking(MainScope().coroutineContext) {
896             provider = ProcessCameraProvider.getInstance(context).await()
897             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
898             val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
899 
900             val singleCameraConfig0 =
901                 SingleCameraConfig(
902                     CameraSelector.DEFAULT_BACK_CAMERA,
903                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
904                     lifecycleOwner0
905                 )
906             val singleCameraConfig1 =
907                 SingleCameraConfig(
908                     CameraSelector.DEFAULT_FRONT_CAMERA,
909                     UseCaseGroup.Builder().addUseCase(useCase1).build(),
910                     lifecycleOwner1
911                 )
912             val singleCameraConfig2 =
913                 SingleCameraConfig(
914                     CameraSelector.DEFAULT_FRONT_CAMERA,
915                     UseCaseGroup.Builder().addUseCase(useCase0).build(),
916                     lifecycleOwner1
917                 )
918 
919             assertThrows<java.lang.IllegalArgumentException> {
920                 provider.bindToLifecycle(
921                     listOf(singleCameraConfig0, singleCameraConfig1, singleCameraConfig2)
922                 )
923             }
924         }
925     }
926 
927     @Test
928     fun bindConcurrentCamera_isDualRecording() {
929         assumeTrue(CameraUtil.hasCameraWithLensFacing(LENS_FACING_FRONT))
930         ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
931 
932         runBlocking(MainScope().coroutineContext) {
933             provider = ProcessCameraProvider.getInstance(context).await()
934             val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
935             val useCase1 =
936                 FakeUseCase(
937                     FakeUseCaseConfig.Builder(CaptureType.VIDEO_CAPTURE).useCaseConfig,
938                     CaptureType.VIDEO_CAPTURE
939                 )
940 
941             val singleCameraConfig0 =
942                 SingleCameraConfig(
943                     CameraSelector.DEFAULT_BACK_CAMERA,
944                     UseCaseGroup.Builder().addUseCase(useCase0).addUseCase(useCase1).build(),
945                     lifecycleOwner0
946                 )
947             val singleCameraConfig1 =
948                 SingleCameraConfig(
949                     CameraSelector.DEFAULT_FRONT_CAMERA,
950                     UseCaseGroup.Builder().addUseCase(useCase0).addUseCase(useCase1).build(),
951                     lifecycleOwner1
952                 )
953 
954             if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
955                 val concurrentCamera =
956                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
957 
958                 assertThat(concurrentCamera).isNotNull()
959                 assertThat(concurrentCamera.cameras.size).isEqualTo(1)
960                 assertThat(provider.isBound(useCase0)).isTrue()
961                 assertThat(provider.isBound(useCase1)).isTrue()
962                 assertThat(provider.isConcurrentCameraModeOn).isTrue()
963             } else {
964                 assertThrows<UnsupportedOperationException> {
965                     provider.bindToLifecycle(listOf(singleCameraConfig0, singleCameraConfig1))
966                 }
967             }
968         }
969     }
970 
971     @Test
972     @SdkSuppress(minSdkVersion = 23)
973     fun bindWithExtensions_doesNotImpactPreviousCamera(): Unit =
974         runBlocking(Dispatchers.Main) {
975             // 1. Arrange.
976             val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
977             val cameraSelectorWithExtensions =
978                 getCameraSelectorWithLimitedCapabilities(
979                     cameraSelector,
980                     emptySet() // All capabilities are not supported.
981                 )
982             ProcessCameraProvider.configureInstance(cameraConfig)
983             provider = ProcessCameraProvider.getInstance(context).await()
984             val useCase = Preview.Builder().build()
985 
986             // 2. Act: bind with and then without Extensions.
987             // bind with regular cameraSelector to get the regular camera (with empty use cases)
988             val camera = provider.bindToLifecycle(lifecycleOwner0, cameraSelector)
989             // bind with extensions cameraSelector to get the restricted version of camera.
990             val cameraWithExtensions =
991                 provider.bindToLifecycle(lifecycleOwner0, cameraSelectorWithExtensions, useCase)
992 
993             // 3. Assert: ensure we can different instances of Camera and one does not affect the
994             // other.
995             assertThat(camera).isNotSameInstanceAs(cameraWithExtensions)
996 
997             // the Extensions CameraControl does not support the zoom.
998             assertThrows<IllegalStateException> {
999                 cameraWithExtensions.cameraControl.setZoomRatio(1.0f).await()
1000             }
1001 
1002             // only the Extensions CameraInfo does not support the zoom.
1003             assertThat(camera.cameraInfo.zoomState.value!!.maxZoomRatio).isGreaterThan(1.0f)
1004             assertThat(cameraWithExtensions.cameraInfo.zoomState.value!!.maxZoomRatio)
1005                 .isEqualTo(1.0f)
1006         }
1007 
1008     @RequiresApi(23)
1009     private fun getCameraSelectorWithLimitedCapabilities(
1010         cameraSelector: CameraSelector,
1011         supportedCapabilities: Set<Int>
1012     ): CameraSelector {
1013         val identifier = Identifier.create("idStr")
1014         val sessionProcessor =
1015             FakeSessionProcessor(supportedCameraOperations = supportedCapabilities)
1016         ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
1017             object : CameraConfig {
1018                 override fun getConfig(): Config {
1019                     return MutableOptionsBundle.create()
1020                 }
1021 
1022                 override fun getCompatibilityId(): Identifier {
1023                     return identifier
1024                 }
1025 
1026                 override fun getSessionProcessor(valueIfMissing: SessionProcessor?) =
1027                     sessionProcessor
1028 
1029                 override fun getSessionProcessor() = sessionProcessor
1030             }
1031         }
1032 
1033         val builder = CameraSelector.Builder.fromSelector(cameraSelector)
1034         builder.addCameraFilter(
1035             object : CameraFilter {
1036                 override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
1037                     val newCameraInfos = mutableListOf<CameraInfo>()
1038                     newCameraInfos.addAll(cameraInfos)
1039                     return newCameraInfos
1040                 }
1041 
1042                 override fun getIdentifier(): Identifier {
1043                     return identifier
1044                 }
1045             }
1046         )
1047 
1048         return builder.build()
1049     }
1050 
1051     private fun createConcurrentCameraAppConfig(): CameraXConfig {
1052         val combination0 =
1053             mapOf(
1054                 "0" to CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build(),
1055                 "1" to CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build()
1056             )
1057         val combination1 =
1058             mapOf(
1059                 "0" to CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build(),
1060                 "2" to CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build()
1061             )
1062 
1063         cameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(combination0)
1064         cameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(combination1)
1065         val cameraFactoryProvider =
1066             CameraFactory.Provider { _, _, _, _ ->
1067                 val cameraFactory = FakeCameraFactory()
1068                 cameraFactory.insertCamera(LENS_FACING_BACK, "0") {
1069                     FakeCamera("0", null, FakeCameraInfoInternal("0", 0, LENS_FACING_BACK))
1070                 }
1071                 cameraFactory.insertCamera(LENS_FACING_FRONT, "1") {
1072                     FakeCamera("1", null, FakeCameraInfoInternal("1", 0, LENS_FACING_FRONT))
1073                 }
1074                 cameraFactory.insertCamera(LENS_FACING_FRONT, "2") {
1075                     FakeCamera("2", null, FakeCameraInfoInternal("2", 0, LENS_FACING_FRONT))
1076                 }
1077                 cameraFactory.cameraCoordinator = cameraCoordinator
1078                 cameraFactory
1079             }
1080         val appConfigBuilder =
1081             CameraXConfig.Builder()
1082                 .setCameraFactoryProvider(cameraFactoryProvider)
1083                 .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
1084                 .setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
1085 
1086         return appConfigBuilder.build()
1087     }
1088 
1089     private fun Rect.aspectRatio(rotationDegrees: Int = 0): Rational {
1090         return if (rotationDegrees % 180 != 0) Rational(height(), width())
1091         else Rational(width(), height())
1092     }
1093 }
1094