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.CameraAccessException;
21 import android.hardware.camera2.cts.CameraTestUtils.HandlerExecutor;
22 import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
23 import android.hardware.camera2.cts.helpers.StaticMetadata;
24 import android.hardware.camera2.cts.helpers.StaticMetadata.CheckLevel;
25 import android.util.Log;
26 import android.os.SystemClock;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.concurrent.ArrayBlockingQueue;
30 import java.util.concurrent.Executor;
31 import java.util.concurrent.TimeUnit;
32 
33 import static org.mockito.Mockito.*;
34 
35 /**
36  * <p>Tests for flashlight API.</p>
37  */
38 public class FlashlightTest extends Camera2AndroidTestCase {
39     private static final String TAG = "FlashlightTest";
40     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
41     private static final int TORCH_DURATION_MS = 1000;
42     private static final int TORCH_TIMEOUT_MS = 3000;
43     private static final int NUM_REGISTERS = 10;
44 
45     private ArrayList<String> mFlashCameraIdList;
46 
47     @Override
setUp()48     protected void setUp() throws Exception {
49         super.setUp();
50 
51         // initialize the list of cameras that have a flash unit so it won't interfere with
52         // flash tests.
53         mFlashCameraIdList = new ArrayList<String>();
54         for (String id : mCameraIds) {
55             StaticMetadata info =
56                     new StaticMetadata(mCameraManager.getCameraCharacteristics(id),
57                                        CheckLevel.ASSERT, /*collector*/ null);
58             if (info.hasFlash()) {
59                 mFlashCameraIdList.add(id);
60             }
61         }
62     }
63 
testSetTorchModeOnOff()64     public void testSetTorchModeOnOff() throws Exception {
65         if (mFlashCameraIdList.size() == 0)
66             return;
67 
68         // reset flash status for all devices with a flash unit
69         for (String id : mFlashCameraIdList) {
70             resetTorchModeStatus(id);
71         }
72 
73         // turn on and off torch mode one by one
74         for (String id : mFlashCameraIdList) {
75             CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
76             mCameraManager.registerTorchCallback(torchListener, mHandler); // should get OFF
77 
78             mCameraManager.setTorchMode(id, true); // should get ON
79             SystemClock.sleep(TORCH_DURATION_MS);
80             mCameraManager.setTorchMode(id, false); // should get OFF
81 
82             // verify corrected numbers of callbacks
83             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
84                     times(2)).onTorchModeChanged(id, false);
85             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
86                     times(mFlashCameraIdList.size() + 1)).
87                     onTorchModeChanged(anyString(), eq(false));
88             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
89                     times(1)).onTorchModeChanged(id, true);
90             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
91                     times(1)).onTorchModeChanged(anyString(), eq(true));
92             verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
93                     onTorchModeUnavailable(anyString());
94 
95             mCameraManager.unregisterTorchCallback(torchListener);
96         }
97 
98         // turn on all torch modes at once
99         if (mFlashCameraIdList.size() >= 2) {
100             CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
101             mCameraManager.registerTorchCallback(torchListener, mHandler); // should get OFF.
102 
103             for (String id : mFlashCameraIdList) {
104                 // should get ON for this ID.
105                 // may get OFF for previously-on IDs.
106                 mCameraManager.setTorchMode(id, true);
107             }
108 
109             SystemClock.sleep(TORCH_DURATION_MS);
110 
111             for (String id : mFlashCameraIdList) {
112                 // should get OFF if not turned off previously.
113                 mCameraManager.setTorchMode(id, false);
114             }
115 
116             verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(mFlashCameraIdList.size())).
117                     onTorchModeChanged(anyString(), eq(true));
118             // one more off for each id due to callback registeration.
119             verify(torchListener, timeout(TORCH_TIMEOUT_MS).
120                     times(mFlashCameraIdList.size() * 2)).
121                     onTorchModeChanged(anyString(), eq(false));
122 
123             mCameraManager.unregisterTorchCallback(torchListener);
124         }
125     }
126 
testTorchCallback()127     public void testTorchCallback() throws Exception {
128         testTorchCallback(/*useExecutor*/ false);
129         testTorchCallback(/*useExecutor*/ true);
130     }
131 
testTorchCallback(boolean useExecutor)132     private void testTorchCallback(boolean useExecutor) throws Exception {
133         if (mFlashCameraIdList.size() == 0)
134             return;
135 
136         final Executor executor = useExecutor ? new HandlerExecutor(mHandler) : null;
137         // reset torch mode status
138         for (String id : mFlashCameraIdList) {
139             resetTorchModeStatus(id);
140         }
141 
142         CameraManager.TorchCallback torchListener = mock(CameraManager.TorchCallback.class);
143 
144         for (int i = 0; i < NUM_REGISTERS; i++) {
145             // should get OFF for all cameras with a flash unit.
146             if (useExecutor) {
147                 mCameraManager.registerTorchCallback(executor, torchListener);
148             } else {
149                 mCameraManager.registerTorchCallback(torchListener, mHandler);
150             }
151             mCameraManager.unregisterTorchCallback(torchListener);
152         }
153 
154         verify(torchListener, timeout(TORCH_TIMEOUT_MS).
155                 times(NUM_REGISTERS * mFlashCameraIdList.size())).
156                 onTorchModeChanged(anyString(), eq(false));
157         verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
158                 onTorchModeChanged(anyString(), eq(true));
159         verify(torchListener, after(TORCH_TIMEOUT_MS).never()).
160                 onTorchModeUnavailable(anyString());
161 
162         // verify passing a null handler will raise IllegalArgumentException
163         try {
164             mCameraManager.registerTorchCallback(torchListener, null);
165             mCameraManager.unregisterTorchCallback(torchListener);
166             fail("should get IllegalArgumentException due to no handler");
167         } catch (IllegalArgumentException e) {
168             // expected exception
169         }
170     }
171 
testCameraDeviceOpenAfterTorchOn()172     public void testCameraDeviceOpenAfterTorchOn() throws Exception {
173         if (mFlashCameraIdList.size() == 0)
174             return;
175 
176         for (String id : mFlashCameraIdList) {
177             for (String idToOpen : mCameraIds) {
178                 resetTorchModeStatus(id);
179 
180                 CameraManager.TorchCallback torchListener =
181                         mock(CameraManager.TorchCallback.class);
182 
183                 // this will trigger OFF for each id in mFlashCameraIdList
184                 mCameraManager.registerTorchCallback(torchListener, mHandler);
185 
186                 // this will trigger ON for id
187                 mCameraManager.setTorchMode(id, true);
188                 SystemClock.sleep(TORCH_DURATION_MS);
189 
190                 // if id == idToOpen, this will trigger UNAVAILABLE and may trigger OFF.
191                 // this may trigger UNAVAILABLE for any other id in mFlashCameraIdList
192                 openDevice(idToOpen);
193 
194                 // if id == idToOpen, this will trigger OFF.
195                 // this may trigger OFF for any other id in mFlashCameraIdList.
196                 closeDevice(idToOpen);
197 
198                 // this may trigger OFF for id if not received previously.
199                 mCameraManager.setTorchMode(id, false);
200 
201                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
202                         onTorchModeChanged(id, true);
203                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
204                         onTorchModeChanged(anyString(), eq(true));
205 
206                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).atLeast(2)).
207                         onTorchModeChanged(id, false);
208                 verify(torchListener, atMost(3)).onTorchModeChanged(id, false);
209 
210                 verify(torchListener, timeout(TORCH_TIMEOUT_MS).
211                         atLeast(mFlashCameraIdList.size())).
212                         onTorchModeChanged(anyString(), eq(false));
213                 verify(torchListener, atMost(mFlashCameraIdList.size() * 2 + 1)).
214                         onTorchModeChanged(anyString(), eq(false));
215 
216                 if (hasFlash(idToOpen)) {
217                     verify(torchListener, timeout(TORCH_TIMEOUT_MS).times(1)).
218                             onTorchModeUnavailable(idToOpen);
219                 }
220                 verify(torchListener, atMost(mFlashCameraIdList.size())).
221                             onTorchModeUnavailable(anyString());
222 
223                 mCameraManager.unregisterTorchCallback(torchListener);
224             }
225         }
226     }
227 
testTorchModeExceptions()228     public void testTorchModeExceptions() throws Exception {
229         // cameraIdsToTestTorch = all available camera ID + non-existing camera id +
230         //                        non-existing numeric camera id + null
231         String[] cameraIdsToTestTorch = new String[mCameraIds.length + 3];
232         System.arraycopy(mCameraIds, 0, cameraIdsToTestTorch, 0, mCameraIds.length);
233         cameraIdsToTestTorch[mCameraIds.length] = generateNonexistingCameraId();
234         cameraIdsToTestTorch[mCameraIds.length + 1] = generateNonexistingNumericCameraId();
235 
236         for (String idToOpen : mCameraIds) {
237             openDevice(idToOpen);
238             try {
239                 for (String id : cameraIdsToTestTorch) {
240                     try {
241                         mCameraManager.setTorchMode(id, true);
242                         SystemClock.sleep(TORCH_DURATION_MS);
243                         mCameraManager.setTorchMode(id, false);
244                         if (!hasFlash(id)) {
245                             fail("exception should be thrown when turning on torch mode of a " +
246                                     "camera without a flash");
247                         } else if (id.equals(idToOpen)) {
248                             fail("exception should be thrown when turning on torch mode of an " +
249                                     "opened camera");
250                         }
251                     } catch (CameraAccessException e) {
252                         if ((hasFlash(id) &&  id.equals(idToOpen) &&
253                                     e.getReason() == CameraAccessException.CAMERA_IN_USE) ||
254                             (hasFlash(id) && !id.equals(idToOpen) &&
255                                     e.getReason() == CameraAccessException.MAX_CAMERAS_IN_USE)) {
256                             continue;
257                         }
258                         fail("(" + id + ") not expecting: " + e.getMessage());
259                     } catch (IllegalArgumentException e) {
260                         if (hasFlash(id)) {
261                             fail("not expecting IllegalArgumentException");
262                         }
263                     }
264                 }
265             } finally {
266                 closeDevice(idToOpen);
267             }
268         }
269     }
270 
hasFlash(String cameraId)271     private boolean hasFlash(String cameraId) {
272         return mFlashCameraIdList.contains(cameraId);
273     }
274 
275     // make sure the torch status is off.
resetTorchModeStatus(String cameraId)276     private void resetTorchModeStatus(String cameraId) throws Exception {
277         TorchCallbackListener torchListener = new TorchCallbackListener(cameraId);
278 
279         mCameraManager.registerTorchCallback(torchListener, mHandler);
280         mCameraManager.setTorchMode(cameraId, true);
281         mCameraManager.setTorchMode(cameraId, false);
282 
283         torchListener.waitOnStatusChange(TorchCallbackListener.STATUS_ON);
284         torchListener.waitOnStatusChange(TorchCallbackListener.STATUS_OFF);
285 
286         mCameraManager.unregisterTorchCallback(torchListener);
287     }
288 
generateNonexistingCameraId()289     private String generateNonexistingCameraId() {
290         String nonExisting = "none_existing_camera";
291         for (String id : mCameraIds) {
292             if (Arrays.asList(mCameraIds).contains(nonExisting)) {
293                 nonExisting += id;
294             } else {
295                 break;
296             }
297         }
298         return nonExisting;
299     }
300 
301     // return a non-existing and non-negative numeric camera id.
generateNonexistingNumericCameraId()302     private String generateNonexistingNumericCameraId() {
303         int[] numericCameraIds = new int[mCameraIds.length];
304         int size = 0;
305 
306         for (String cameraId : mCameraIds) {
307             try {
308                 int value = Integer.parseInt(cameraId);
309                 if (value >= 0) {
310                     numericCameraIds[size++] = value;
311                 }
312             } catch (Throwable e) {
313                 // do nothing if camera id isn't an integer
314             }
315         }
316 
317         if (size == 0) {
318             return "0";
319         }
320 
321         Arrays.sort(numericCameraIds, 0, size);
322         if (numericCameraIds[0] != 0) {
323             return "0";
324         }
325 
326         for (int i = 0; i < size - 1; i++) {
327             if (numericCameraIds[i] + 1 < numericCameraIds[i + 1]) {
328                 return String.valueOf(numericCameraIds[i] + 1);
329             }
330         }
331 
332         if (numericCameraIds[size - 1] != Integer.MAX_VALUE) {
333             return String.valueOf(numericCameraIds[size - 1] + 1);
334         }
335 
336         fail("cannot find a non-existing and non-negative numeric camera id");
337         return null;
338     }
339 
340     private final class TorchCallbackListener extends CameraManager.TorchCallback {
341         private static final String TAG = "TorchCallbackListener";
342         private static final int STATUS_WAIT_TIMEOUT_MS = 3000;
343         private static final int QUEUE_CAPACITY = 100;
344 
345         private String mCameraId;
346         private ArrayBlockingQueue<Integer> mStatusQueue =
347                 new ArrayBlockingQueue<Integer>(QUEUE_CAPACITY);
348 
349         public static final int STATUS_UNAVAILABLE = 0;
350         public static final int STATUS_OFF = 1;
351         public static final int STATUS_ON = 2;
352 
TorchCallbackListener(String cameraId)353         public TorchCallbackListener(String cameraId) {
354             // only care about events for this camera id.
355             mCameraId = cameraId;
356         }
357 
waitOnStatusChange(int status)358         public void waitOnStatusChange(int status) throws Exception {
359             while (true) {
360                 Integer s = mStatusQueue.poll(STATUS_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
361                 if (s == null) {
362                     fail("waiting for status " + status + " timed out");
363                 } else if (s.intValue() == status) {
364                     return;
365                 }
366             }
367         }
368 
369         @Override
onTorchModeUnavailable(String cameraId)370         public void onTorchModeUnavailable(String cameraId) {
371             if (cameraId.equals(mCameraId)) {
372                 Integer s = new Integer(STATUS_UNAVAILABLE);
373                 try {
374                     mStatusQueue.put(s);
375                 } catch (Throwable e) {
376                     fail(e.getMessage());
377                 }
378             }
379         }
380 
381         @Override
onTorchModeChanged(String cameraId, boolean enabled)382         public void onTorchModeChanged(String cameraId, boolean enabled) {
383             if (cameraId.equals(mCameraId)) {
384                 Integer s;
385                 if (enabled) {
386                     s = new Integer(STATUS_ON);
387                 } else {
388                     s = new Integer(STATUS_OFF);
389                 }
390                 try {
391                     mStatusQueue.put(s);
392                 } catch (Throwable e) {
393                     fail(e.getMessage());
394                 }
395             }
396         }
397     }
398 }
399