1 /*
2  * 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.camera2.internal;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 import static org.junit.Assume.assumeFalse;
24 import static org.junit.Assume.assumeTrue;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.reset;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 
30 import android.content.Context;
31 import android.graphics.Rect;
32 import android.hardware.camera2.CameraAccessException;
33 import android.hardware.camera2.CameraCharacteristics;
34 import android.hardware.camera2.CaptureRequest;
35 import android.os.Handler;
36 import android.os.HandlerThread;
37 
38 import androidx.camera.camera2.Camera2Config;
39 import androidx.camera.camera2.impl.Camera2ImplConfig;
40 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
41 import androidx.camera.core.CameraInfoUnavailableException;
42 import androidx.camera.core.CameraSelector;
43 import androidx.camera.core.CameraXConfig;
44 import androidx.camera.core.ZoomState;
45 import androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback;
46 import androidx.camera.core.impl.SessionConfig;
47 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
48 import androidx.camera.testing.impl.CameraUtil;
49 import androidx.camera.testing.impl.CameraXUtil;
50 import androidx.camera.testing.impl.HandlerUtil;
51 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner;
52 import androidx.core.os.HandlerCompat;
53 import androidx.test.annotation.UiThreadTest;
54 import androidx.test.core.app.ApplicationProvider;
55 import androidx.test.ext.junit.runners.AndroidJUnit4;
56 import androidx.test.filters.SdkSuppress;
57 import androidx.test.filters.SmallTest;
58 
59 import com.google.common.util.concurrent.ListenableFuture;
60 
61 import org.jspecify.annotations.NonNull;
62 import org.junit.After;
63 import org.junit.Assert;
64 import org.junit.Before;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 import org.mockito.Mockito;
68 
69 import java.util.Objects;
70 import java.util.concurrent.CountDownLatch;
71 import java.util.concurrent.ExecutionException;
72 import java.util.concurrent.ScheduledExecutorService;
73 import java.util.concurrent.TimeUnit;
74 import java.util.concurrent.TimeoutException;
75 
76 @SmallTest
77 @RunWith(AndroidJUnit4.class)
78 @SdkSuppress(minSdkVersion = 21)
79 @SuppressWarnings("ConstantConditions") // We might hit an NPE, which is fine. It's a test.
80 public final class ZoomControlDeviceTest {
81     private static final int TOLERANCE = 5;
82     private ZoomControl mZoomControl;
83     private Camera2CameraControlImpl mCamera2CameraControlImpl;
84     private HandlerThread mHandlerThread;
85     private ControlUpdateCallback mControlUpdateCallback;
86     private CameraCharacteristics mCameraCharacteristics;
87     private CameraCharacteristicsCompat mCameraCharacteristicsCompat;
88     private Handler mHandler;
89 
90     @Before
setUp()91     public void setUp()
92             throws CameraInfoUnavailableException, CameraAccessException, InterruptedException {
93         assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK));
94 
95         // Init CameraX
96         Context context = ApplicationProvider.getApplicationContext();
97         CameraXConfig config = Camera2Config.defaultConfig();
98         CameraXUtil.initialize(context, config);
99 
100         mCameraCharacteristics =
101                 CameraUtil.getCameraCharacteristics(CameraSelector.LENS_FACING_BACK);
102 
103         mControlUpdateCallback = mock(ControlUpdateCallback.class);
104         mHandlerThread = new HandlerThread("ControlThread");
105         mHandlerThread.start();
106         mHandler = HandlerCompat.createAsync(mHandlerThread.getLooper());
107 
108         ScheduledExecutorService executorService = CameraXExecutors.newHandlerExecutor(mHandler);
109         String cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK);
110         mCameraCharacteristicsCompat = CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
111                 mCameraCharacteristics, cameraId);
112         assumeTrue(getMaxDigitalZoom() >= 2.0);
113 
114         mCamera2CameraControlImpl = new Camera2CameraControlImpl(mCameraCharacteristicsCompat,
115                 executorService, executorService, mControlUpdateCallback);
116 
117         mZoomControl = mCamera2CameraControlImpl.getZoomControl();
118         mZoomControl.setActive(true);
119 
120         // Await Camera2CameraControlImpl updateSessionConfig to complete.
121         HandlerUtil.waitForLooperToIdle(mHandler);
122         Mockito.reset(mControlUpdateCallback);
123     }
124 
125     @After
tearDown()126     public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
127         CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS);
128         if (mHandlerThread != null) {
129             mHandlerThread.quit();
130         }
131     }
132 
isAndroidRZoomEnabled()133     private boolean isAndroidRZoomEnabled() {
134         return ZoomControl.isAndroidRZoomSupported(mCameraCharacteristicsCompat);
135     }
136 
137     @Test
138     @UiThreadTest
setZoomRatio_getValueIsCorrect_InUIThread()139     public void setZoomRatio_getValueIsCorrect_InUIThread() {
140         final float newZoomRatio = 2.0f;
141         assumeTrue(newZoomRatio <= mZoomControl.getZoomState().getValue().getMaxZoomRatio());
142 
143         // We can only ensure new value is reflected immediately on getZoomRatio on UI thread
144         // because of the nature of LiveData.
145         mZoomControl.setZoomRatio(newZoomRatio);
146         assertThat(mZoomControl.getZoomState().getValue().getZoomRatio()).isEqualTo(newZoomRatio);
147     }
148 
149     @Test
150     @UiThreadTest
setZoomRatio_largerThanMax_zoomUnmodified()151     public void setZoomRatio_largerThanMax_zoomUnmodified() {
152         assumeTrue(2.0f <= mZoomControl.getZoomState().getValue().getMaxZoomRatio());
153         mZoomControl.setZoomRatio(2.0f);
154         float maxZoomRatio = mZoomControl.getZoomState().getValue().getMaxZoomRatio();
155         mZoomControl.setZoomRatio(maxZoomRatio + 1.0f);
156         assertThat(mZoomControl.getZoomState().getValue().getZoomRatio()).isEqualTo(2.0f);
157     }
158 
159     @Test
setZoomRatio_largerThanMax_OutOfRangeException()160     public void setZoomRatio_largerThanMax_OutOfRangeException() {
161         float maxZoomRatio = mZoomControl.getZoomState().getValue().getMaxZoomRatio();
162         ListenableFuture<Void> result = mZoomControl.setZoomRatio(maxZoomRatio + 1.0f);
163 
164         assertThrowOutOfRangeExceptionOnListenableFuture(result);
165     }
166 
assertThrowOutOfRangeExceptionOnListenableFuture(ListenableFuture<Void> result)167     private void assertThrowOutOfRangeExceptionOnListenableFuture(ListenableFuture<Void> result) {
168         try {
169             result.get(100, TimeUnit.MILLISECONDS);
170         } catch (InterruptedException | TimeoutException ignored) {
171         } catch (ExecutionException ee) {
172             assertThat(ee.getCause()).isInstanceOf(IllegalArgumentException.class);
173             return;
174         }
175 
176         fail();
177     }
178 
179     @Test
180     @UiThreadTest
setZoomRatio_smallerThanMin_zoomUnmodified()181     public void setZoomRatio_smallerThanMin_zoomUnmodified() {
182         assumeTrue(2.0f <= mZoomControl.getZoomState().getValue().getMaxZoomRatio());
183         mZoomControl.setZoomRatio(2.0f);
184         float minZoomRatio = mZoomControl.getZoomState().getValue().getMinZoomRatio();
185         mZoomControl.setZoomRatio(minZoomRatio - 1.0f);
186         assertThat(mZoomControl.getZoomState().getValue().getZoomRatio()).isEqualTo(2.0f);
187     }
188 
189     @Test
setZoomRatio_smallerThanMin_OutOfRangeException()190     public void setZoomRatio_smallerThanMin_OutOfRangeException() {
191         float minZoomRatio = mZoomControl.getZoomState().getValue().getMinZoomRatio();
192         ListenableFuture<Void> result = mZoomControl.setZoomRatio(minZoomRatio - 1.0f);
193         assertThrowOutOfRangeExceptionOnListenableFuture(result);
194     }
195 
getSensorRect()196     private Rect getSensorRect() {
197         Rect rect = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
198         // Some device like pixel 2 will have (0, 8) as the left-top corner.
199         return new Rect(0, 0, rect.width(), rect.height());
200     }
201 
202     @Test
setZoomRatioBy1_0_isEqualToSensorRect()203     public void setZoomRatioBy1_0_isEqualToSensorRect() throws InterruptedException {
204         assumeFalse(isAndroidRZoomEnabled());
205         mZoomControl.setZoomRatio(1.0f);
206         HandlerUtil.waitForLooperToIdle(mHandler);
207         Rect sessionCropRegion = getSessionCropRegion(mControlUpdateCallback);
208         assertThat(sessionCropRegion).isEqualTo(getSensorRect());
209     }
210 
211     @Test
212     @SdkSuppress(minSdkVersion = 30)
setZoomRatioBy1_0_androidRZoomRatioIsUpdated()213     public void setZoomRatioBy1_0_androidRZoomRatioIsUpdated() throws InterruptedException {
214         assumeTrue(isAndroidRZoomEnabled());
215         mZoomControl.setZoomRatio(1.0f);
216         HandlerUtil.waitForLooperToIdle(mHandler);
217         float zoomRatio = getAndroidRZoomRatio(mControlUpdateCallback);
218         assertThat(zoomRatio).isEqualTo(1.0f);
219     }
220 
221     @Test
setZoomRatioBy2_0_cropRegionIsSetCorrectly()222     public void setZoomRatioBy2_0_cropRegionIsSetCorrectly() throws InterruptedException {
223         assumeFalse(isAndroidRZoomEnabled());
224         mZoomControl.setZoomRatio(2.0f);
225         HandlerUtil.waitForLooperToIdle(mHandler);
226 
227         Rect sessionCropRegion = getSessionCropRegion(mControlUpdateCallback);
228 
229         Rect sensorRect = getSensorRect();
230         int cropX = (sensorRect.width() / 4);
231         int cropY = (sensorRect.height() / 4);
232         Rect cropRect = new Rect(cropX, cropY, cropX + sensorRect.width() / 2,
233                 cropY + sensorRect.height() / 2);
234         assertThat(sessionCropRegion).isEqualTo(cropRect);
235     }
236 
237     @Test
238     @SdkSuppress(minSdkVersion = 30)
setZoomRatioBy2_0_androidRZoomRatioIsUpdated()239     public void setZoomRatioBy2_0_androidRZoomRatioIsUpdated() throws InterruptedException {
240         assumeTrue(isAndroidRZoomEnabled());
241         mZoomControl.setZoomRatio(2.0f);
242         HandlerUtil.waitForLooperToIdle(mHandler);
243 
244         float zoomRatio = getAndroidRZoomRatio(mControlUpdateCallback);
245         assertThat(zoomRatio).isEqualTo(2.0f);
246     }
247 
getSessionCropRegion(ControlUpdateCallback controlUpdateCallback)248     private @NonNull Rect getSessionCropRegion(ControlUpdateCallback controlUpdateCallback) {
249         verify(controlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
250         SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
251         Camera2ImplConfig camera2Config = new Camera2ImplConfig(
252                 sessionConfig.getImplementationOptions());
253 
254         reset(controlUpdateCallback);
255         return Objects.requireNonNull(camera2Config.getCaptureRequestOption(
256                 CaptureRequest.SCALER_CROP_REGION, null));
257     }
258 
259     @SdkSuppress(minSdkVersion = 30)
getAndroidRZoomRatio(ControlUpdateCallback controlUpdateCallback)260     private @NonNull Float getAndroidRZoomRatio(ControlUpdateCallback controlUpdateCallback) {
261         verify(controlUpdateCallback, times(1)).onCameraControlUpdateSessionConfig();
262         SessionConfig sessionConfig = mCamera2CameraControlImpl.getSessionConfig();
263         Camera2ImplConfig camera2Config = new Camera2ImplConfig(
264                 sessionConfig.getImplementationOptions());
265 
266         reset(controlUpdateCallback);
267         assertThat(camera2Config.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
268                 .isNull();
269         return Objects.requireNonNull(camera2Config.getCaptureRequestOption(
270                 CaptureRequest.CONTROL_ZOOM_RATIO, null));
271     }
272 
273     @UiThreadTest
274     @Test
setLinearZoomBy0_isSameAsMinRatio()275     public void setLinearZoomBy0_isSameAsMinRatio() {
276         mZoomControl.setLinearZoom(0);
277         float ratioAtPercentage0 = mZoomControl.getZoomState().getValue().getZoomRatio();
278 
279         mZoomControl.setZoomRatio(mZoomControl.getZoomState().getValue().getMinZoomRatio());
280         float ratioAtMinZoomRatio = mZoomControl.getZoomState().getValue().getZoomRatio();
281 
282         assertThat(ratioAtPercentage0).isEqualTo(ratioAtMinZoomRatio);
283     }
284 
285     @UiThreadTest
286     @Test
setLinearZoomBy1_isSameAsMaxRatio()287     public void setLinearZoomBy1_isSameAsMaxRatio() {
288         mZoomControl.setLinearZoom(1);
289         float ratioAtPercentage1 = mZoomControl.getZoomState().getValue().getZoomRatio();
290 
291         mZoomControl.setZoomRatio(mZoomControl.getZoomState().getValue().getMaxZoomRatio());
292         float ratioAtMaxZoomRatio = mZoomControl.getZoomState().getValue().getZoomRatio();
293 
294         assertThat(ratioAtPercentage1).isEqualTo(ratioAtMaxZoomRatio);
295     }
296 
297     @UiThreadTest
298     @Test
setLinearZoomBy0_5_isHalfCropWidth()299     public void setLinearZoomBy0_5_isHalfCropWidth() throws InterruptedException {
300         assumeFalse(isAndroidRZoomEnabled());
301 
302         mZoomControl.setLinearZoom(1f);
303         HandlerUtil.waitForLooperToIdle(mHandler);
304         Rect cropRegionMaxZoom = getSessionCropRegion(mControlUpdateCallback);
305 
306         Rect cropRegionMinZoom = getSensorRect();
307 
308         mZoomControl.setLinearZoom(0.5f);
309         HandlerUtil.waitForLooperToIdle(mHandler);
310         Rect cropRegionHalfZoom = getSessionCropRegion(mControlUpdateCallback);
311 
312         Assert.assertEquals(cropRegionHalfZoom.width(),
313                 (cropRegionMinZoom.width() + cropRegionMaxZoom.width()) / 2.0f, TOLERANCE);
314     }
315 
316     @UiThreadTest
317     @Test
318     @SdkSuppress(minSdkVersion = 30)
setLinearZoomBy0_5_androidRZoomRatioUpdatedCorrectly()319     public void setLinearZoomBy0_5_androidRZoomRatioUpdatedCorrectly() throws InterruptedException {
320         assumeTrue(isAndroidRZoomEnabled());
321 
322         mZoomControl.setLinearZoom(1f);
323         HandlerUtil.waitForLooperToIdle(mHandler);
324         float zoomRatioForLinearMax = getAndroidRZoomRatio(mControlUpdateCallback);
325         final float cropWidth = 10000f;
326         float cropWidthForLinearMax = cropWidth / zoomRatioForLinearMax;
327 
328         mZoomControl.setLinearZoom(0f);
329         HandlerUtil.waitForLooperToIdle(mHandler);
330         float zoomRatioForLinearMin = getAndroidRZoomRatio(mControlUpdateCallback);
331         float cropWidthForLinearMin = cropWidth / zoomRatioForLinearMin;
332 
333         mZoomControl.setLinearZoom(0.5f);
334         HandlerUtil.waitForLooperToIdle(mHandler);
335         float zoomRatioForLinearHalf = getAndroidRZoomRatio(mControlUpdateCallback);
336         float cropWidthForLinearHalf = cropWidth / zoomRatioForLinearHalf;
337 
338         Assert.assertEquals(cropWidthForLinearHalf,
339                 (cropWidthForLinearMin + cropWidthForLinearMax) / 2.0f, TOLERANCE);
340     }
341 
342     @UiThreadTest
343     @Test
setLinearZoom_cropWidthChangedLinearly()344     public void setLinearZoom_cropWidthChangedLinearly() throws InterruptedException {
345         assumeFalse(isAndroidRZoomEnabled());
346 
347         // crop region in percentage == 0 is null, need to use sensor rect instead.
348         Rect prevCropRegion = getSensorRect();
349 
350         float prevWidthDelta = 0;
351         for (float percentage = 0.1f; percentage < 1.0f; percentage += 0.1f) {
352 
353             mZoomControl.setLinearZoom(percentage);
354             HandlerUtil.waitForLooperToIdle(mHandler);
355             Rect cropRegion = getSessionCropRegion(mControlUpdateCallback);
356 
357             if (prevWidthDelta == 0) {
358                 prevWidthDelta = prevCropRegion.width() - cropRegion.width();
359             } else {
360                 float widthDelta = prevCropRegion.width() - cropRegion.width();
361                 Assert.assertEquals(prevWidthDelta, widthDelta, TOLERANCE);
362             }
363 
364             prevCropRegion = cropRegion;
365         }
366     }
367 
368     @UiThreadTest
369     @Test
370     @SdkSuppress(minSdkVersion = 30)
setLinearZoom_androidRZoomRatio_cropWidthChangedLinearly()371     public void setLinearZoom_androidRZoomRatio_cropWidthChangedLinearly()
372             throws InterruptedException {
373         assumeTrue(isAndroidRZoomEnabled());
374         final float cropWidth = 10000;
375 
376         mZoomControl.setLinearZoom(0f);
377         HandlerUtil.waitForLooperToIdle(mHandler);
378         float zoomRatioForLinearMin = getAndroidRZoomRatio(mControlUpdateCallback);
379 
380         float prevCropWidth = cropWidth / zoomRatioForLinearMin;
381 
382         float prevWidthDelta = 0;
383         for (float percentage = 0.1f; percentage < 1.0f; percentage += 0.1f) {
384 
385             mZoomControl.setLinearZoom(percentage);
386             HandlerUtil.waitForLooperToIdle(mHandler);
387             float zoomRatio = getAndroidRZoomRatio(mControlUpdateCallback);
388             float cropWidthForTheRatio = cropWidth / zoomRatio;
389 
390             if (prevWidthDelta == 0) {
391                 prevWidthDelta = prevCropWidth - cropWidthForTheRatio;
392             } else {
393                 float widthDelta = prevCropWidth - cropWidthForTheRatio;
394                 Assert.assertEquals(prevWidthDelta, widthDelta, TOLERANCE);
395             }
396 
397             prevCropWidth = cropWidthForTheRatio;
398         }
399     }
400 
401     @UiThreadTest
402     @Test
setLinearZoom_largerThan1_zoomUnmodified()403     public void setLinearZoom_largerThan1_zoomUnmodified() {
404         mZoomControl.setLinearZoom(0.5f);
405         mZoomControl.setLinearZoom(1.1f);
406         assertThat(mZoomControl.getZoomState().getValue().getLinearZoom()).isEqualTo(0.5f);
407     }
408 
409     @Test
setLinearZoom_largerThan1_outOfRangeException()410     public void setLinearZoom_largerThan1_outOfRangeException() {
411         ListenableFuture<Void> result = mZoomControl.setLinearZoom(1.1f);
412         assertThrowOutOfRangeExceptionOnListenableFuture(result);
413     }
414 
415     @UiThreadTest
416     @Test
setLinearZoom_smallerThan0_zoomUnmodified()417     public void setLinearZoom_smallerThan0_zoomUnmodified() {
418         mZoomControl.setLinearZoom(0.5f);
419         mZoomControl.setLinearZoom(-0.1f);
420         assertThat(mZoomControl.getZoomState().getValue().getLinearZoom()).isEqualTo(0.5f);
421     }
422 
423     @Test
setLinearZoom_smallerThan0_outOfRangeException()424     public void setLinearZoom_smallerThan0_outOfRangeException() {
425         ListenableFuture<Void> result = mZoomControl.setLinearZoom(-0.1f);
426         assertThrowOutOfRangeExceptionOnListenableFuture(result);
427     }
428 
429     @UiThreadTest
430     @Test
getterLiveData_defaultValueIsNonNull()431     public void getterLiveData_defaultValueIsNonNull() {
432         assertThat(mZoomControl.getZoomState().getValue()).isNotNull();
433     }
434 
435     @UiThreadTest
436     @Test
getZoomRatioLiveData_observerIsCalledWhenZoomRatioIsSet()437     public void getZoomRatioLiveData_observerIsCalledWhenZoomRatioIsSet()
438             throws InterruptedException {
439         CountDownLatch latch1 = new CountDownLatch(1);
440         CountDownLatch latch2 = new CountDownLatch(1);
441         CountDownLatch latch3 = new CountDownLatch(1);
442         FakeLifecycleOwner lifecycleOwner = new FakeLifecycleOwner();
443         lifecycleOwner.startAndResume();
444 
445         mZoomControl.getZoomState().observe(lifecycleOwner, (value) -> {
446             if (value.getZoomRatio() == 1.2f) {
447                 latch1.countDown();
448             } else if (value.getZoomRatio() == 1.5f) {
449                 latch2.countDown();
450             } else if (value.getZoomRatio() == 2.0f) {
451                 latch3.countDown();
452             }
453         });
454 
455         mZoomControl.setZoomRatio(1.2f);
456         mZoomControl.setZoomRatio(1.5f);
457         mZoomControl.setZoomRatio(2.0f);
458 
459         assertTrue(latch1.await(500, TimeUnit.MILLISECONDS));
460         assertTrue(latch2.await(500, TimeUnit.MILLISECONDS));
461         assertTrue(latch3.await(500, TimeUnit.MILLISECONDS));
462     }
463 
464     @UiThreadTest
465     @Test
getZoomRatioLiveData_observerIsCalledWhenZoomPercentageIsSet()466     public void getZoomRatioLiveData_observerIsCalledWhenZoomPercentageIsSet()
467             throws InterruptedException {
468         CountDownLatch latch = new CountDownLatch(3);
469         FakeLifecycleOwner lifecycleOwner = new FakeLifecycleOwner();
470         lifecycleOwner.startAndResume();
471 
472         mZoomControl.getZoomState().observe(lifecycleOwner, (value) -> {
473             if (value.getZoomRatio() != 1.0f) {
474                 latch.countDown();
475             }
476         });
477 
478         mZoomControl.setLinearZoom(0.1f);
479         mZoomControl.setLinearZoom(0.2f);
480         mZoomControl.setLinearZoom(0.3f);
481 
482         assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
483     }
484 
485     @UiThreadTest
486     @Test
getZoomPercentageLiveData_observerIsCalledWhenZoomPercentageIsSet()487     public void getZoomPercentageLiveData_observerIsCalledWhenZoomPercentageIsSet()
488             throws InterruptedException {
489         CountDownLatch latch1 = new CountDownLatch(1);
490         CountDownLatch latch2 = new CountDownLatch(1);
491         CountDownLatch latch3 = new CountDownLatch(1);
492         FakeLifecycleOwner lifecycleOwner = new FakeLifecycleOwner();
493         lifecycleOwner.startAndResume();
494 
495         mZoomControl.getZoomState().observe(lifecycleOwner, (value) -> {
496             if (value.getLinearZoom() == 0.1f) {
497                 latch1.countDown();
498             } else if (value.getLinearZoom() == 0.2f) {
499                 latch2.countDown();
500             } else if (value.getLinearZoom() == 0.3f) {
501                 latch3.countDown();
502             }
503         });
504 
505         mZoomControl.setLinearZoom(0.1f);
506         mZoomControl.setLinearZoom(0.2f);
507         mZoomControl.setLinearZoom(0.3f);
508 
509         assertTrue(latch1.await(500, TimeUnit.MILLISECONDS));
510         assertTrue(latch2.await(500, TimeUnit.MILLISECONDS));
511         assertTrue(latch3.await(500, TimeUnit.MILLISECONDS));
512     }
513 
514     @UiThreadTest
515     @Test
getZoomPercentageLiveData_observerIsCalledWhenZoomRatioIsSet()516     public void getZoomPercentageLiveData_observerIsCalledWhenZoomRatioIsSet()
517             throws InterruptedException {
518         CountDownLatch latch = new CountDownLatch(3);
519         FakeLifecycleOwner lifecycleOwner = new FakeLifecycleOwner();
520         lifecycleOwner.startAndResume();
521 
522         mZoomControl.getZoomState().observe(lifecycleOwner, (value) -> {
523             if (value.getLinearZoom() != 0f) {
524                 latch.countDown();
525             }
526         });
527 
528         mZoomControl.setZoomRatio(1.2f);
529         mZoomControl.setZoomRatio(1.5f);
530         mZoomControl.setZoomRatio(2.0f);
531 
532         assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
533     }
534 
535     @UiThreadTest
536     @Test
getZoomRatioDefaultValue()537     public void getZoomRatioDefaultValue() {
538         assertThat(mZoomControl.getZoomState().getValue().getZoomRatio()).isEqualTo(
539                 ZoomControl.DEFAULT_ZOOM_RATIO);
540     }
541 
542     @UiThreadTest
543     @Test
getZoomPercentageDefaultValue()544     public void getZoomPercentageDefaultValue() {
545         assumeFalse(isAndroidRZoomEnabled());
546         assertThat(mZoomControl.getZoomState().getValue().getLinearZoom()).isEqualTo(0);
547     }
548 
549     @UiThreadTest
550     @Test
getMaxZoomRatio_isMaxDigitalZoom()551     public void getMaxZoomRatio_isMaxDigitalZoom() {
552         float maxZoom = mZoomControl.getZoomState().getValue().getMaxZoomRatio();
553         assertThat(maxZoom).isEqualTo(getMaxDigitalZoom());
554     }
555 
556     @UiThreadTest
557     @Test
getMinZoomRatio_isOne()558     public void getMinZoomRatio_isOne() {
559         assumeFalse(isAndroidRZoomEnabled());
560         float minZoom = mZoomControl.getZoomState().getValue().getMinZoomRatio();
561         assertThat(minZoom).isEqualTo(1f);
562     }
563 
getMaxDigitalZoom()564     private float getMaxDigitalZoom() {
565         if (isAndroidRZoomEnabled()) {
566             return mCameraCharacteristics.get(
567                     CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getUpper();
568         }
569         return mCameraCharacteristics.get(
570                 CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
571     }
572 
573     @Test
getMaxZoomRatio_isEqualToMaxDigitalZoom()574     public void getMaxZoomRatio_isEqualToMaxDigitalZoom() {
575         float maxZoom = mZoomControl.getZoomState().getValue().getMaxZoomRatio();
576 
577         assertThat(maxZoom).isEqualTo(getMaxDigitalZoom());
578     }
579 
580     @UiThreadTest
581     @Test
valueIsResetAfterInactive()582     public void valueIsResetAfterInactive() {
583         mZoomControl.setActive(true);
584         mZoomControl.setLinearZoom(0.2f); // this will change ratio and percentage.
585 
586         mZoomControl.setActive(false);
587 
588         assertThat(mZoomControl.getZoomState().getValue().getZoomRatio()).isEqualTo(
589                 ZoomControl.DEFAULT_ZOOM_RATIO);
590     }
591 
592     @Test
maxZoomShouldBeLargerThanOrEqualToMinZoom()593     public void maxZoomShouldBeLargerThanOrEqualToMinZoom() {
594         ZoomState zoomState = mZoomControl.getZoomState().getValue();
595         assertThat(zoomState.getMaxZoomRatio()).isAtLeast(zoomState.getMinZoomRatio());
596     }
597 }
598