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