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