1 /*
2  * Copyright 2015 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 android.hardware.camera2.cts;
18 
19 import android.hardware.camera2.CameraManager;
20 import android.hardware.camera2.CameraCharacteristics;
21 import android.hardware.camera2.CameraAccessException;
22 import android.hardware.camera2.cts.CameraTestUtils.HandlerExecutor;
23 import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
24 import android.hardware.camera2.cts.helpers.StaticMetadata;
25 import android.hardware.camera2.cts.helpers.StaticMetadata.CheckLevel;
26 import android.util.Log;
27 import android.os.Build;
28 import android.os.SystemClock;
29 import android.platform.test.annotations.AppModeFull;
30 import com.android.compatibility.common.util.PropertyUtil;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.concurrent.ArrayBlockingQueue;
34 import java.util.concurrent.Executor;
35 import java.util.concurrent.TimeUnit;
36 
37 import static org.mockito.Mockito.*;
38 import org.junit.runners.Parameterized;
39 import org.junit.runner.RunWith;
40 import org.junit.Test;
41 
42 import static junit.framework.Assert.*;
43 
44 /**
45  * <p>Tests for flashlight API.</p>
46  */
47 
48 @RunWith(Parameterized.class)
49 public class FlashlightTest extends Camera2AndroidTestCase {
50     private static final String TAG = "FlashlightTest";
51     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
52     private static final int TORCH_DURATION_MS = 1000;
53     private static final int TORCH_TIMEOUT_MS = 3000;
54     private static final int NUM_REGISTERS = 10;
55 
56     private ArrayList<String> mFlashCameraIdList;
57     private ArrayList<String> mNoFlashCameraIdList;
58 
59     @Override
setUp()60     public void setUp() throws Exception {
61         //Use all camera ids for system camera testing since we count the number of callbacks here
62         // and when mAdoptShellPerm == true, all camera ids will get callbacks.
63         super.setUp(/*useAll*/true);
64 
65         // initialize the list of cameras that have a flash unit so it won't interfere with
66         // flash tests.
67         mFlashCameraIdList = new ArrayList<String>();
68         mNoFlashCameraIdList = new ArrayList<String>();
69         for (String id : getCameraIdsUnderTest()) {
70             StaticMetadata info =
71                     new StaticMetadata(mCameraManager.getCameraCharacteristics(id),
72                                        CheckLevel.ASSERT, /*collector*/ null);
73             if (info.hasFlash()) {
74                 mFlashCameraIdList.add(id);
75             } else  {
76                 mNoFlashCameraIdList.add(id);
77             }
78         }
79     }
80 
81     @Test
testTurnOnTorchWithStrengthLevel()82     public void testTurnOnTorchWithStrengthLevel() throws Exception {
83         if (mNoFlashCameraIdList.size() != 0) {
84             for (String id : mNoFlashCameraIdList) {
85                 CameraCharacteristics pc = mCameraManager.getCameraCharacteristics(id);
86                 assertNull(pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL));
87                 assertNull(pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL));
88             }
89         }
90 
91         if (mFlashCameraIdList.size() == 0)
92             return;
93 
94         for (String id : mFlashCameraIdList) {
95             resetTorchModeStatus(id);
96         }
97 
98         for (String id: mFlashCameraIdList) {
99             int maxLevel = 0;
100             int defaultLevel = 0;
101             int minLevel = 1;
102             CameraCharacteristics pc = mCameraManager.getCameraCharacteristics(id);
103             if (pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL) != null) {
104                 defaultLevel = pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL);
105             }
106             if (pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) != null) {
107                 maxLevel = pc.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL);
108             }
109             if (maxLevel > 1) {
110                 assertTrue(minLevel <= defaultLevel);
111                 assertTrue(defaultLevel <= maxLevel);
112                 int torchStrength = 0;
113                 CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
114                 mCameraManager.registerTorchCallback(torchListener, mHandler);
115 
116                 mCameraManager.turnOnTorchWithStrengthLevel(id, maxLevel);
117                 SystemClock.sleep(TORCH_DURATION_MS);
118                 torchStrength = mCameraManager.getTorchStrengthLevel(id);
119                 assertEquals(torchStrength, maxLevel);
120                 // Calling with same value twice to verify onTorchStrengthLevelChanged()
121                 // with maxLevel value is called only once.
122                 mCameraManager.turnOnTorchWithStrengthLevel(id, maxLevel);
123                 torchStrength = mCameraManager.getTorchStrengthLevel(id);
124                 assertEquals(torchStrength, maxLevel);
125 
126                 mCameraManager.turnOnTorchWithStrengthLevel(id, defaultLevel);
127                 torchStrength = mCameraManager.getTorchStrengthLevel(id);
128                 assertEquals(torchStrength, defaultLevel);
129 
130                 mCameraManager.turnOnTorchWithStrengthLevel(id, minLevel);
131                 torchStrength = mCameraManager.getTorchStrengthLevel(id);
132                 assertEquals(torchStrength, minLevel);
133 
134                 try {
135                     mCameraManager.turnOnTorchWithStrengthLevel(id, 0);
136                     fail("turnOnTorchWithStrengthLevel with strengthLevel = 0 must fail.");
137                 } catch (IllegalArgumentException e) {
138                     Log.v(TAG, e.getMessage());
139                 }
140 
141                 try {
142                     mCameraManager.turnOnTorchWithStrengthLevel(id, maxLevel + 1);
143                     fail("turnOnTorchWithStrengthLevel with strengthLevel" + (maxLevel + 1) + " must fail.");
144                 } catch (IllegalArgumentException e) {
145                     Log.v(TAG, e.getMessage());
146                 }
147 
148                 // Turn off the torch and verify if the strength level gets
149                 // reset to default level.
150                 mCameraManager.setTorchMode(id, false);
151                 torchStrength = mCameraManager.getTorchStrengthLevel(id);
152                 assertEquals(torchStrength, defaultLevel);
153 
154                 // verify corrected numbers of callbacks
155                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).
156                         times(1)).onTorchModeChanged(id, true);
157 
158                 verify(torchListener,timeout(TORCH_TIMEOUT_MS).
159                         times(1)).onTorchStrengthLevelChanged(id, maxLevel);
160                 verify(torchListener,timeout(TORCH_TIMEOUT_MS).
161                         times(1)).onTorchStrengthLevelChanged(id, minLevel);
162                 verify(torchListener,timeout(TORCH_TIMEOUT_MS).
163                         times(1)).onTorchStrengthLevelChanged(id, defaultLevel);
164 
165                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).
166                         times(2)).onTorchModeChanged(id, false);
167 
168                 mCameraManager.unregisterTorchCallback(torchListener);
169             } else {
170                 Log.i(TAG, "Torch strength level adjustment is not supported.");
171             }
172         }
173     }
174 
175 
176     @Test
testSetTorchModeOnOff()177     public void testSetTorchModeOnOff() throws Exception {
178         if (mFlashCameraIdList.size() == 0)
179             return;
180 
181         // reset flash status for all devices with a flash unit
182         for (String id : mFlashCameraIdList) {
183             resetTorchModeStatus(id);
184         }
185 
186         // turn on and off torch mode one by one
187         for (String id : mFlashCameraIdList) {
188             CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
189             mCameraManager.registerTorchCallback(torchListener, mHandler); // should get OFF
190 
191             mCameraManager.setTorchMode(id, true); // should get ON
192             SystemClock.sleep(TORCH_DURATION_MS);
193             mCameraManager.setTorchMode(id, false); // should get OFF
194 
195             // verify corrected numbers of callbacks
196             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
197                     times(2)).onTorchModeChanged(id, false);
198             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
199                     times(mFlashCameraIdList.size() + 1)).
200                     onTorchModeChanged(anyString(), eq(false));
201             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
202                     times(1)).onTorchModeChanged(id, true);
203             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
204                     times(1)).onTorchModeChanged(anyString(), eq(true));
205             verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
206                     onTorchModeUnavailable(anyString());
207 
208             mCameraManager.unregisterTorchCallback(torchListener);
209         }
210 
211         // turn on all torch modes at once
212         if (mFlashCameraIdList.size() >= 2) {
213             CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
214             mCameraManager.registerTorchCallback(torchListener, mHandler); // should get OFF.
215 
216             for (String id : mFlashCameraIdList) {
217                 // should get ON for this ID.
218                 // may get OFF for previously-on IDs.
219                 mCameraManager.setTorchMode(id, true);
220             }
221 
222             SystemClock.sleep(TORCH_DURATION_MS);
223 
224             for (String id : mFlashCameraIdList) {
225                 // should get OFF if not turned off previously.
226                 mCameraManager.setTorchMode(id, false);
227             }
228 
229             verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(mFlashCameraIdList.size())).
230                     onTorchModeChanged(anyString(), eq(true));
231             // one more off for each id due to callback registeration.
232             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
233                     times(mFlashCameraIdList.size() * 2)).
234                     onTorchModeChanged(anyString(), eq(false));
235 
236             mCameraManager.unregisterTorchCallback(torchListener);
237         }
238     }
239 
240     @Test
testTorchCallback()241     public void testTorchCallback() throws Exception {
242         testTorchCallback(/*useExecutor*/ false);
243         testTorchCallback(/*useExecutor*/ true);
244     }
245 
testTorchCallback(boolean useExecutor)246     private void testTorchCallback(boolean useExecutor) throws Exception {
247         if (mFlashCameraIdList.size() == 0)
248             return;
249 
250         final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null;
251         // reset torch mode status
252         for (String id : mFlashCameraIdList) {
253             resetTorchModeStatus(id);
254         }
255 
256         CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
257 
258         for (int i = 0; i < NUM_REGISTERS; i++) {
259             // should get OFF for all cameras with a flash unit.
260             if (useExecutor) {
261                 mCameraManager.registerTorchCallback(executor, torchListener);
262             } else {
263                 mCameraManager.registerTorchCallback(torchListener, mHandler);
264             }
265             mCameraManager.unregisterTorchCallback(torchListener);
266         }
267 
268         verify(torchListener, timeout(TORCH_TIMEOUT_MS).
269                 times(NUM_REGISTERS * mFlashCameraIdList.size())).
270                 onTorchModeChanged(anyString(), eq(false));
271         verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
272                 onTorchModeChanged(anyString(), eq(true));
273         verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
274                 onTorchModeUnavailable(anyString());
275 
276         // verify passing a null handler will raise IllegalArgumentException
277         try {
278             mCameraManager.registerTorchCallback(torchListener, null);
279             mCameraManager.unregisterTorchCallback(torchListener);
280             fail("should get IllegalArgumentException due to no handler");
281         } catch (IllegalArgumentException e) {
282             // expected exception
283         }
284     }
285 
286     @Test
287     @AppModeFull(reason = "PropertyUtil methods don't work for instant apps")
testCameraDeviceOpenAfterTorchOn()288     public void testCameraDeviceOpenAfterTorchOn() throws Exception {
289         if (mFlashCameraIdList.size() == 0)
290             return;
291 
292         for (String id : mFlashCameraIdList) {
293             for (String idToOpen : getCameraIdsUnderTest()) {
294                 resetTorchModeStatus(id);
295 
296                 CameraManager.TorchCallback torchListener =
297                         mock(CameraManager.TorchCallback.class);
298 
299                 // this will trigger OFF for each id in mFlashCameraIdList
300                 mCameraManager.registerTorchCallback(torchListener, mHandler);
301 
302                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
303                         onTorchModeChanged(id, /*enabled*/false);
304 
305                 // this will trigger ON for id
306                 mCameraManager.setTorchMode(id, true);
307                 SystemClock.sleep(TORCH_DURATION_MS);
308 
309                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
310                         onTorchModeChanged(id, true);
311 
312                 // if id == idToOpen, this will trigger UNAVAILABLE.
313                 // this may trigger UNAVAILABLE for any other id in mFlashCameraIdList
314                 openDevice(idToOpen);
315 
316                 try {
317                     if (PropertyUtil.getVendorApiLevel() > Build.VERSION_CODES.TIRAMISU) {
318                         // Opening a camera device shouldn't result in
319                         // onTorchModeChanged() being called. The number of
320                         // invocations should remain at 1 for both ON and OFF.
321                         verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
322                                 onTorchModeChanged(id, false);
323                         verify(torchListener, after(TORCH_TIMEOUT_MS).times(1)).
324                                 onTorchModeChanged(id, true);
325                     }
326 
327                     // if id == idToOpen, this will trigger OFF.
328                     // this may trigger OFF for any other id in mFlashCameraIdList.
329                     closeDevice(idToOpen);
330 
331                     // this may trigger OFF for id if not received previously.
332                     mCameraManager.setTorchMode(id, false);
333 
334                     verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
335                             onTorchModeChanged(id, true);
336                     verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
337                             onTorchModeChanged(anyString(), eq(true));
338 
339                     verify(torchListener, timeout(TORCH_TIMEOUT_MS).atLeast(2)).
340                             onTorchModeChanged(id, false);
341                     verify(torchListener, atMost(3)).onTorchModeChanged(id, false);
342 
343                     verify(torchListener, timeout(TORCH_TIMEOUT_MS).
344                             atLeast(mFlashCameraIdList.size())).
345                             onTorchModeChanged(anyString(), eq(false));
346                     verify(torchListener, atMost(mFlashCameraIdList.size() * 2 + 1)).
347                             onTorchModeChanged(anyString(), eq(false));
348 
349                     if (hasFlash(idToOpen)) {
350                         verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
351                                 onTorchModeUnavailable(idToOpen);
352                     }
353                     verify(torchListener, atMost(mFlashCameraIdList.size())).
354                                 onTorchModeUnavailable(anyString());
355 
356                     mCameraManager.unregisterTorchCallback(torchListener);
357                 } finally {
358                     closeDevice(idToOpen);
359                 }
360             }
361         }
362     }
363 
364     @Test
testTorchModeExceptions()365     public void testTorchModeExceptions() throws Exception {
366         // cameraIdsToTestTorch = all available camera ID + non-existing camera id +
367         //                        non-existing numeric camera id + null
368         String[] cameraIdsUnderTest = getCameraIdsUnderTest();
369         String[] cameraIdsToTestTorch = new String[cameraIdsUnderTest.length + 3];
370         System.arraycopy(cameraIdsUnderTest, 0, cameraIdsToTestTorch, 0, cameraIdsUnderTest.length);
371         cameraIdsToTestTorch[cameraIdsUnderTest.length] = generateNonexistingCameraId();
372         cameraIdsToTestTorch[cameraIdsUnderTest.length + 1] = generateNonexistingNumericCameraId();
373 
374         for (String idToOpen : cameraIdsUnderTest) {
375             openDevice(idToOpen);
376             try {
377                 for (String id : cameraIdsToTestTorch) {
378                     try {
379                         mCameraManager.setTorchMode(id, true);
380                         SystemClock.sleep(TORCH_DURATION_MS);
381                         mCameraManager.setTorchMode(id, false);
382                         if (!hasFlash(id)) {
383                             fail("exception should be thrown when turning on torch mode of a " +
384                                     "camera without a flash");
385                         } else if (id.equals(idToOpen)) {
386                             fail("exception should be thrown when turning on torch mode of an " +
387                                     "opened camera");
388                         }
389                     } catch (CameraAccessException e) {
390                         int reason = e.getReason();
391                         if ((hasFlash(id) &&  id.equals(idToOpen) &&
392                                     reason == CameraAccessException.CAMERA_IN_USE) ||
393                             (hasFlash(id) && !id.equals(idToOpen) &&
394                                     reason == CameraAccessException.MAX_CAMERAS_IN_USE)) {
395                             continue;
396                         }
397                         fail("(" + id + ") not expecting: " + e.getMessage() + "reason " + reason);
398                     } catch (IllegalArgumentException e) {
399                         if (hasFlash(id)) {
400                             fail("not expecting IllegalArgumentException");
401                         }
402                     }
403                 }
404             } finally {
405                 closeDevice(idToOpen);
406             }
407         }
408     }
409 
hasFlash(String cameraId)410     private boolean hasFlash(String cameraId) {
411         return mFlashCameraIdList.contains(cameraId);
412     }
413 
414     // make sure the torch status is off.
resetTorchModeStatus(String cameraId)415     private void resetTorchModeStatus(String cameraId) throws Exception {
416         TorchCallbackListener torchListener = new TorchCallbackListener(cameraId);
417 
418         mCameraManager.registerTorchCallback(torchListener, mHandler);
419         mCameraManager.setTorchMode(cameraId, true);
420         mCameraManager.setTorchMode(cameraId, false);
421 
422         torchListener.waitOnStatusChange(TorchCallbackListener.STATUS_ON);
423         torchListener.waitOnStatusChange(TorchCallbackListener.STATUS_OFF);
424 
425         mCameraManager.unregisterTorchCallback(torchListener);
426     }
427 
generateNonexistingCameraId()428     private String generateNonexistingCameraId() throws Exception {
429         String nonExisting = "none_existing_camera";
430         String[] cameraIdsUnderTest = getCameraIdsUnderTest();
431         for (String id : cameraIdsUnderTest) {
432             if (Arrays.asList(cameraIdsUnderTest).contains(nonExisting)) {
433                 nonExisting += id;
434             } else {
435                 break;
436             }
437         }
438         return nonExisting;
439     }
440 
441     // return a non-existing and non-negative numeric camera id.
generateNonexistingNumericCameraId()442     private String generateNonexistingNumericCameraId() throws Exception {
443         // We don't rely on getCameraIdsUnderTest() to generate a non existing camera id since
444         // it doesn't give us an accurate reflection of which camera ids actually
445         // exist. It just tells us the ones we're testing right now.
446         String[] allCameraIds = mCameraManager.getCameraIdListNoLazy();
447         int[] numericCameraIds = new int[allCameraIds.length];
448         int size = 0;
449 
450         for (String cameraId : allCameraIds) {
451             try {
452                 int value = Integer.parseInt(cameraId);
453                 if (value >= 0) {
454                     numericCameraIds[size++] = value;
455                 }
456             } catch (Throwable e) {
457                 // do nothing if camera id isn't an integer
458             }
459         }
460 
461         if (size == 0) {
462             return "0";
463         }
464 
465         Arrays.sort(numericCameraIds, 0, size);
466         if (numericCameraIds[0] != 0) {
467             return "0";
468         }
469 
470         for (int i = 0; i < size - 1; i++) {
471             if (numericCameraIds[i] + 1 < numericCameraIds[i + 1]) {
472                 return String.valueOf(numericCameraIds[i] + 1);
473             }
474         }
475 
476         if (numericCameraIds[size - 1] != Integer.MAX_VALUE) {
477             return String.valueOf(numericCameraIds[size - 1] + 1);
478         }
479 
480         fail("cannot find a non-existing and non-negative numeric camera id");
481         return null;
482     }
483 
484     private final class TorchCallbackListener extends CameraManager.TorchCallback {
485         private static final String TAG = "TorchCallbackListener";
486         private static final int STATUS_WAIT_TIMEOUT_MS = 3000;
487         private static final int QUEUE_CAPACITY = 100;
488 
489         private String mCameraId;
490         private ArrayBlockingQueue<Integer> mStatusQueue =
491                 new ArrayBlockingQueue<Integer>(QUEUE_CAPACITY);
492         private ArrayBlockingQueue<Integer> mTorchStrengthQueue =
493                 new ArrayBlockingQueue<Integer>(QUEUE_CAPACITY);
494 
495         public static final int STATUS_UNAVAILABLE = 0;
496         public static final int STATUS_OFF = 1;
497         public static final int STATUS_ON = 2;
498 
TorchCallbackListener(String cameraId)499         public TorchCallbackListener(String cameraId) {
500             // only care about events for this camera id.
501             mCameraId = cameraId;
502         }
503 
waitOnStatusChange(int status)504         public void waitOnStatusChange(int status) throws Exception {
505             while (true) {
506                 Integer s = mStatusQueue.poll(STATUS_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
507                 if (s == null) {
508                     fail("waiting for status " + status + " timed out");
509                 } else if (s.intValue() == status) {
510                     return;
511                 }
512             }
513         }
514 
515         @Override
onTorchModeUnavailable(String cameraId)516         public void onTorchModeUnavailable(String cameraId) {
517             if (cameraId.equals(mCameraId)) {
518                 Integer s = new Integer(STATUS_UNAVAILABLE);
519                 try {
520                     mStatusQueue.put(s);
521                 } catch (Throwable e) {
522                     fail(e.getMessage());
523                 }
524             }
525         }
526 
527         @Override
onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel)528         public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel) {
529             if (cameraId.equals(mCameraId)) {
530                 try {
531                     mTorchStrengthQueue.put(newStrengthLevel);
532                 } catch (Throwable e) {
533                     fail(e.getMessage());
534                 }
535             }
536         }
537 
538         @Override
onTorchModeChanged(String cameraId, boolean enabled)539         public void onTorchModeChanged(String cameraId, boolean enabled) {
540             if (cameraId.equals(mCameraId)) {
541                 Integer s;
542                 if (enabled) {
543                     s = new Integer(STATUS_ON);
544                 } else {
545                     s = new Integer(STATUS_OFF);
546                 }
547                 try {
548                     mStatusQueue.put(s);
549                 } catch (Throwable e) {
550                     fail(e.getMessage());
551                 }
552             }
553         }
554     }
555 }
556