1 /*
2  * Copyright 2020 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 static android.hardware.camera2.cts.CameraTestUtils.*;
20 import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.*;
21 
22 import android.graphics.ImageFormat;
23 import android.graphics.SurfaceTexture;
24 import android.hardware.camera2.CameraCaptureSession;
25 import android.hardware.camera2.CameraDevice;
26 import android.hardware.camera2.CaptureRequest;
27 import android.hardware.camera2.CaptureResult;
28 import android.hardware.camera2.TotalCaptureResult;
29 import android.hardware.camera2.CaptureFailure;
30 import android.hardware.camera2.cts.helpers.StaticMetadata;
31 import android.hardware.camera2.cts.testcases.Camera2ConcurrentAndroidTestCase;
32 import android.hardware.camera2.params.OutputConfiguration;
33 import android.hardware.camera2.params.MandatoryStreamCombination;
34 import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation;
35 import android.hardware.camera2.params.SessionConfiguration;
36 import android.media.ImageReader;
37 import android.util.Log;
38 import android.util.Pair;
39 import android.view.Surface;
40 
41 import com.android.ex.camera2.blocking.BlockingSessionCallback;
42 
43 import java.util.Arrays;
44 import java.util.ArrayList;
45 import java.util.Comparator;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.HashMap;
49 import java.util.Set;
50 
51 import org.junit.runners.Parameterized;
52 import org.junit.runner.RunWith;
53 import org.junit.Test;
54 
55 import static junit.framework.Assert.assertTrue;
56 import static org.mockito.Mockito.*;
57 
58 /**
59  * Tests exercising concurrent camera streaming mandatory stream combinations.
60  */
61 
62 @RunWith(Parameterized.class)
63 public class ConcurrentCameraTest extends Camera2ConcurrentAndroidTestCase {
64     private static final String TAG = "ConcurrentCameraTest";
65     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
66 
67     /**
68      * Class representing a per camera id test sample.
69      */
70     private static class TestSample {
71         public String cameraId;
72         public StaticMetadata staticInfo;
73         public MandatoryStreamCombination combination;
74         public boolean haveSession = false;
75         public boolean substituteY8;
76         public List<OutputConfiguration> outputConfigs = new ArrayList<OutputConfiguration>();
77         public List<Surface> outputSurfaces = new ArrayList<Surface>();
78         public StreamCombinationTargets targets = new StreamCombinationTargets();
TestSample(String cameraId, StaticMetadata staticInfo, MandatoryStreamCombination combination, boolean subY8)79         public TestSample(String cameraId, StaticMetadata staticInfo,
80                 MandatoryStreamCombination combination, boolean subY8) {
81             this.cameraId = cameraId;
82             this.staticInfo = staticInfo;
83             this.combination = combination;
84             this.substituteY8 = subY8;
85         }
86     }
87 
88     /**
89      * Class representing the information needed to generate a unique combination per camera id
90      */
91     private static class GeneratedEntry {
92         public String cameraId;
93         public boolean substituteY8;
94         MandatoryStreamCombination combination;
95         //Note: We don't have physical camera ids here since physical camera ids do not have
96         //      guaranteed concurrent stream combinations.
GeneratedEntry(String cameraId, boolean substituteY8, MandatoryStreamCombination combination)97         GeneratedEntry(String cameraId, boolean substituteY8,
98                 MandatoryStreamCombination combination) {
99             this.cameraId = cameraId;
100             this.substituteY8 = substituteY8;
101             this.combination = combination;
102         }
103     };
104 
105     @Test
testMandatoryConcurrentStreamCombination()106     public void testMandatoryConcurrentStreamCombination() throws Exception {
107         for (Set<String> cameraIdCombinations : mConcurrentCameraIdCombinations) {
108             if (cameraIdCombinations.size() == 0) {
109                 continue;
110             }
111             List<HashMap<String, GeneratedEntry>> streamCombinationPermutations =
112                     generateStreamSelections(cameraIdCombinations);
113 
114             for (HashMap<String, GeneratedEntry> streamCombinationPermutation :
115                     streamCombinationPermutations) {
116                 ArrayList<TestSample> testSamples = new ArrayList<TestSample>();
117                 for (Map.Entry<String, GeneratedEntry> deviceSample :
118                         streamCombinationPermutation.entrySet()) {
119                     CameraTestInfo info = mCameraTestInfos.get(deviceSample.getKey());
120                     assertTrue("CameraTestInfo not found for camera id " + deviceSample.getKey(),
121                             info != null);
122                     MandatoryStreamCombination chosenCombination =
123                             deviceSample.getValue().combination;
124                     boolean substituteY8 = deviceSample.getValue().substituteY8;
125                     TestSample testSample = new TestSample(deviceSample.getKey(), info.mStaticInfo,
126                             chosenCombination, substituteY8);
127                     testSamples.add(testSample);
128                     openDevice(deviceSample.getKey());
129                 }
130                 try {
131                     testMandatoryConcurrentStreamCombination(testSamples);
132                 } finally {
133                     for (TestSample testSample : testSamples) {
134                         closeDevice(testSample.cameraId);
135                     }
136                 }
137             }
138         }
139     }
140 
141     // @return Generates a List of HashMaps<String, GeneratedEntry> which is a map:
142     //         cameraId -> GeneratedEntry (represents a camera device's test configuration)
143     // @param  perIdGeneratedEntries a per-camera id set of combinations to recursively generate
144     //         GeneratedEntries for.
generateStreamSelectionsInternal( HashMap<String, List<GeneratedEntry>> perIdGeneratedEntries)145     private List<HashMap<String, GeneratedEntry>> generateStreamSelectionsInternal(
146             HashMap<String, List<GeneratedEntry>> perIdGeneratedEntries) {
147         Set<String> cameraIds = perIdGeneratedEntries.keySet();
148         // Base condition for recursion.
149         if (cameraIds.size() == 1) {
150             String cameraId = cameraIds.toArray(new String[1])[0];
151             List<HashMap<String, GeneratedEntry>> ret =
152                     new ArrayList<HashMap<String, GeneratedEntry>> ();
153             for (GeneratedEntry entry : perIdGeneratedEntries.get(cameraId)) {
154                 HashMap<String, GeneratedEntry> retPut = new HashMap<String, GeneratedEntry>();
155                 retPut.put(cameraId, entry);
156                 ret.add(retPut);
157             }
158             return ret;
159         }
160         // Choose one camera id, create all combinations for the remaining set and then add each of
161         // the camera id's combinations to the returned list of maps.
162         String keyChosen = null;
163         for (String cameraId: cameraIds) {
164             keyChosen = cameraId;
165             break;
166         }
167         List<GeneratedEntry> selfGeneratedEntries = perIdGeneratedEntries.get(keyChosen);
168         perIdGeneratedEntries.remove(keyChosen);
169         List<HashMap<String, GeneratedEntry>> recResult =
170                 generateStreamSelectionsInternal(perIdGeneratedEntries);
171         List<HashMap<String, GeneratedEntry>> res =
172                 new ArrayList<HashMap<String, GeneratedEntry>>();
173         for (GeneratedEntry gen : selfGeneratedEntries) {
174             for (HashMap<String, GeneratedEntry> entryMap : recResult) {
175                 // Make a copy of the HashMap, add the generated entry to it and add it to the final
176                 // result (since we want to use the original for the other 'gen' entries)
177                 HashMap<String, GeneratedEntry> copy = (HashMap)entryMap.clone();
178                 copy.put(keyChosen, gen);
179                 res.add(copy);
180             }
181         }
182         return res;
183     }
184 
185     /**
186      * Generates a list of combinations used for mandatory stream combination testing.
187      * Each combination(GeneratedEntry) corresponds to a camera id advertised by
188      * getConcurrentCameraIds().
189      */
generateStreamSelections( Set<String> cameraIdCombination)190     private List<HashMap<String, GeneratedEntry>> generateStreamSelections(
191             Set<String> cameraIdCombination) {
192         // First artificially create lists of GeneratedEntries for each camera id in the passed
193         // set.
194         HashMap<String, List<GeneratedEntry>> perIdGeneratedEntries =
195                 new HashMap<String, List<GeneratedEntry>>();
196         for (String cameraId : cameraIdCombination) {
197             List<GeneratedEntry> genEntries = getGeneratedEntriesFor(cameraId);
198             perIdGeneratedEntries.put(cameraId, genEntries);
199         }
200         return generateStreamSelectionsInternal(perIdGeneratedEntries);
201     }
202 
203     // get GeneratedEntries for a particular camera id.
getGeneratedEntriesFor(String cameraId)204     List<GeneratedEntry> getGeneratedEntriesFor(String cameraId) {
205         CameraTestInfo info = mCameraTestInfos.get(cameraId);
206         assertTrue("CameraTestInfo not found for camera id " + cameraId, info != null);
207         MandatoryStreamCombination[] combinations = info.mMandatoryStreamCombinations;
208         List<GeneratedEntry> generatedEntries = new ArrayList<GeneratedEntry>();
209 
210         // Now generate entries on the camera's mandatory streams
211         for (MandatoryStreamCombination combination : combinations) {
212             generatedEntries.add(new GeneratedEntry(cameraId,/*substituteY8*/false, combination));
213             // Check whether substituting YUV_888 format with Y8 format
214             boolean substituteY8 = false;
215             if (info.mStaticInfo.isMonochromeWithY8()) {
216                 List<MandatoryStreamInformation> streamsInfo = combination.getStreamsInformation();
217                 for (MandatoryStreamInformation streamInfo : streamsInfo) {
218                     if (streamInfo.getFormat() == ImageFormat.YUV_420_888) {
219                         substituteY8 = true;
220                         break;
221                     }
222                 }
223             }
224 
225             if (substituteY8) {
226                 generatedEntries.add(new GeneratedEntry(cameraId, /*substituteY8*/true,
227                           combination));
228             }
229         }
230         return generatedEntries;
231     }
232 
testMandatoryConcurrentStreamCombination(ArrayList<TestSample> testSamples)233     private void testMandatoryConcurrentStreamCombination(ArrayList<TestSample> testSamples)
234             throws Exception {
235 
236         final int TIMEOUT_FOR_RESULT_MS = 1000;
237         final int MIN_RESULT_COUNT = 3;
238         HashMap<String, SessionConfiguration> testSessionMap =
239                 new HashMap<String, SessionConfiguration>();
240         for (TestSample testSample : testSamples) {
241             CameraTestInfo info = mCameraTestInfos.get(testSample.cameraId);
242             assertTrue("CameraTestInfo not found for camera id " + testSample.cameraId,
243                     info != null);
244             List<OutputConfiguration> outputConfigs = new ArrayList<>();
245             CameraTestUtils.setupConfigurationTargets(
246                 testSample.combination.getStreamsInformation(), testSample.targets,
247                 outputConfigs, testSample.outputSurfaces, MIN_RESULT_COUNT,
248                 testSample.substituteY8, /*substituteHEIC*/false, /*physicalCameraId*/null,
249                 /*multiResStreamConfig*/null, mHandler);
250             for (OutputConfiguration c : outputConfigs) {
251                 testSample.outputConfigs.add(c);
252             }
253 
254             try {
255                 checkSessionConfigurationSupported(info.mCamera, mHandler, testSample.outputConfigs,
256                         /*inputConfig*/ null, SessionConfiguration.SESSION_REGULAR,
257                         mCameraManager, true/*defaultSupport*/, String.format(
258                         "Session configuration query from combination: %s failed",
259                         testSample.combination.getDescription()));
260                 testSessionMap.put(testSample.cameraId, new SessionConfiguration(
261                       SessionConfiguration.SESSION_REGULAR,testSample.outputConfigs,
262                       new HandlerExecutor(mHandler), new BlockingSessionCallback()));
263             } catch (Throwable e) {
264                 mCollector.addMessage(String.format(
265                         "Mandatory stream combination: %s for camera id %s failed due: %s",
266                         testSample.combination.getDescription(), testSample.cameraId,
267                         e.getMessage()));
268             }
269         }
270 
271         // Mandatory stream combinations must be reported as supported.
272         assertTrue("Concurrent session configs not supported",
273                 mCameraManager.isConcurrentSessionConfigurationSupported(testSessionMap));
274 
275         for (TestSample testSample  : testSamples) {
276             try {
277                 CameraTestInfo info = mCameraTestInfos.get(testSample.cameraId);
278                 assertTrue("CameraTestInfo not found for camera id " + testSample.cameraId,
279                         info != null);
280                 createSessionByConfigs(testSample.cameraId, testSample.outputConfigs);
281                 testSample.haveSession = true;
282                 CaptureRequest.Builder requestBuilder =
283                         info.mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
284 
285                 for (OutputConfiguration c : testSample.outputConfigs) {
286                     requestBuilder.addTarget(c.getSurface());
287                 }
288                 CaptureRequest request = requestBuilder.build();
289                 CameraCaptureSession.CaptureCallback mockCaptureCallback =
290                         mock(CameraCaptureSession.CaptureCallback.class);
291                 info.mCameraSession.setRepeatingRequest(request, mockCaptureCallback, mHandler);
292 
293                 verify(mockCaptureCallback,
294                         timeout(TIMEOUT_FOR_RESULT_MS * MIN_RESULT_COUNT).atLeast(MIN_RESULT_COUNT))
295                         .onCaptureCompleted(
296                             eq(info.mCameraSession),
297                             eq(request),
298                             isA(TotalCaptureResult.class));
299                 verify(mockCaptureCallback, never()).
300                         onCaptureFailed(
301                             eq(info.mCameraSession),
302                             eq(request),
303                             isA(CaptureFailure.class));
304             } catch (Throwable e) {
305                 mCollector.addMessage(String.format(
306                         "Mandatory stream combination : %s for camera id %s failed due: %s",
307                         testSample.combination.getDescription(),
308                         testSample.cameraId, e.getMessage()));
309             }
310 
311         }
312 
313         for (TestSample testSample : testSamples) {
314             if (testSample.haveSession) {
315                 try {
316                     Log.i(TAG,
317                             String.format("Done with camera %s, combination: %s, closing session",
318                             testSample.cameraId, testSample.combination.getDescription()));
319                     stopCapture(testSample.cameraId, /*fast*/false);
320                 } catch (Throwable e) {
321                     String closingDownFormat =
322                             "Closing down for combination: %s  for camera id %s failed due to: %s";
323                     mCollector.addMessage(
324                             String.format(closingDownFormat,
325                             testSample.combination.getDescription(), testSample.cameraId,
326                             e.getMessage()));
327                 }
328             }
329             testSample.targets.close();
330         }
331     }
332 }
333