1 /*
2  * Copyright 2023 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 androidx.camera.core.internal;
18 
19 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
20 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
21 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
22 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
23 import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
24 
25 import android.graphics.ImageFormat;
26 import android.util.Pair;
27 import android.util.Rational;
28 import android.util.Size;
29 import android.view.Surface;
30 
31 import androidx.camera.core.AspectRatio;
32 import androidx.camera.core.CameraSelector;
33 import androidx.camera.core.Logger;
34 import androidx.camera.core.impl.CameraInfoInternal;
35 import androidx.camera.core.impl.ImageOutputConfig;
36 import androidx.camera.core.impl.UseCaseConfig;
37 import androidx.camera.core.impl.utils.AspectRatioUtil;
38 import androidx.camera.core.impl.utils.CameraOrientationUtil;
39 import androidx.camera.core.impl.utils.CompareSizesByArea;
40 import androidx.camera.core.internal.utils.SizeUtil;
41 import androidx.camera.core.resolutionselector.AspectRatioStrategy;
42 import androidx.camera.core.resolutionselector.ResolutionFilter;
43 import androidx.camera.core.resolutionselector.ResolutionSelector;
44 import androidx.camera.core.resolutionselector.ResolutionStrategy;
45 
46 import org.jspecify.annotations.NonNull;
47 import org.jspecify.annotations.Nullable;
48 
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.LinkedHashMap;
54 import java.util.List;
55 import java.util.Map;
56 
57 /**
58  * The supported output sizes collector to help collect the available resolution candidate list
59  * according to the use case config and the following settings in {@link ResolutionSelector}:
60  *
61  * <ul>
62  *   <li>Aspect ratio strategy
63  *   <li>Resolution strategy
64  *   <li>Custom resolution filter
65  *   <li>High resolution enabled flags
66  * </ul>
67  */
68 public class SupportedOutputSizesSorter {
69     private static final String TAG = "SupportedOutputSizesCollector";
70     private final CameraInfoInternal mCameraInfoInternal;
71     private final int mSensorOrientation;
72     private final int mLensFacing;
73     private final Rational mFullFovRatio;
74     private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
75 
SupportedOutputSizesSorter(@onNull CameraInfoInternal cameraInfoInternal, @Nullable Size activeArraySize)76     public SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
77             @Nullable Size activeArraySize) {
78         mCameraInfoInternal = cameraInfoInternal;
79         mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
80         mLensFacing = mCameraInfoInternal.getLensFacing();
81         mFullFovRatio = activeArraySize != null ? calculateFullFovRatioFromActiveArraySize(
82                 activeArraySize) : calculateFullFovRatioFromSupportedOutputSizes(
83                 mCameraInfoInternal);
84         mSupportedOutputSizesSorterLegacy =
85                 new SupportedOutputSizesSorterLegacy(cameraInfoInternal, mFullFovRatio);
86     }
87 
88     /**
89      * Calculates the full FOV ratio by the active array size.
90      */
calculateFullFovRatioFromActiveArraySize( @onNull Size activeArraySize)91     private @NonNull Rational calculateFullFovRatioFromActiveArraySize(
92             @NonNull Size activeArraySize) {
93         return new Rational(activeArraySize.getWidth(), activeArraySize.getHeight());
94     }
95 
96     /**
97      * Calculates the full FOV ratio by the output sizes retrieved from CameraInfoInternal.
98      *
99      * <p>For most devices, the full FOV ratio should match the aspect ratio of the max supported
100      * output sizes. The active pixel array info is not used because it may cause robolectric
101      * test to fail if it is not set in the test environment.
102      */
calculateFullFovRatioFromSupportedOutputSizes( @onNull CameraInfoInternal cameraInfoInternal)103     private @Nullable Rational calculateFullFovRatioFromSupportedOutputSizes(
104             @NonNull CameraInfoInternal cameraInfoInternal) {
105         List<Size> jpegOutputSizes = cameraInfoInternal.getSupportedResolutions(ImageFormat.JPEG);
106         if (jpegOutputSizes.isEmpty()) {
107             return null;
108         }
109         Size maxSize = Collections.max(jpegOutputSizes, new CompareSizesByArea());
110         return new Rational(maxSize.getWidth(), maxSize.getHeight());
111     }
112 
113     /**
114      * Returns the sorted output sizes according to the use case config.
115      *
116      * <p>If ResolutionSelector is specified in the use case config, the output sizes will be
117      * sorted according to the ResolutionSelector setting and logic. Otherwise, the output sizes
118      * will be sorted according to the legacy resolution API settings and logic.
119      */
getSortedSupportedOutputSizes( @onNull UseCaseConfig<?> useCaseConfig)120     public @NonNull List<Size> getSortedSupportedOutputSizes(
121             @NonNull UseCaseConfig<?> useCaseConfig) {
122         ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
123         List<Size> customOrderedResolutions = imageOutputConfig.getCustomOrderedResolutions(null);
124 
125         // Directly returns the custom ordered resolutions list if it is set.
126         if (customOrderedResolutions != null) {
127             return customOrderedResolutions;
128         }
129 
130         ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
131         List<Pair<Integer, Size[]>>  customResolutions =
132                 imageOutputConfig.getSupportedResolutions(null);
133         List<Size> candidateSizes = getResolutionCandidateList(customResolutions,
134                 useCaseConfig.getInputFormat());
135 
136         if (resolutionSelector == null) {
137             return mSupportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
138                     candidateSizes, useCaseConfig);
139         } else {
140             Size maxResolution = ((ImageOutputConfig) useCaseConfig).getMaxResolution(null);
141             int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
142             // Applies the high resolution settings onto the resolution candidate list.
143             if (!useCaseConfig.isHighResolutionDisabled(false)) {
144                 candidateSizes = applyHighResolutionSettings(candidateSizes,
145                         resolutionSelector, useCaseConfig.getInputFormat());
146             }
147             return sortSupportedOutputSizesByResolutionSelector(
148                     imageOutputConfig.getResolutionSelector(),
149                     candidateSizes,
150                     maxResolution,
151                     targetRotation,
152                     mFullFovRatio,
153                     mSensorOrientation,
154                     mLensFacing);
155         }
156     }
157 
getSizeListByFormat( @ullable List<Pair<Integer, Size[]>> resolutionsPairList, int imageFormat)158     private @Nullable List<Size> getSizeListByFormat(
159             @Nullable List<Pair<Integer, Size[]>> resolutionsPairList,
160             int imageFormat) {
161         Size[] outputSizes = null;
162 
163         if (resolutionsPairList != null) {
164             for (Pair<Integer, Size[]> formatResolutionPair : resolutionsPairList) {
165                 if (formatResolutionPair.first == imageFormat) {
166                     outputSizes = formatResolutionPair.second;
167                     break;
168                 }
169             }
170         }
171         return outputSizes == null ? null : Arrays.asList(outputSizes);
172     }
173 
174     /**
175      * Sorts the resolution candidate list according to the ResolutionSelector API logic.
176      *
177      * <ol>
178      *   <li>Applies the aspect ratio strategy
179      *     <ul>
180      *       <li>Applies the aspect ratio strategy fallback rule
181      *     </ul>
182      *   <li>Applies the resolution strategy
183      *     <ul>
184      *       <li>Applies the resolution strategy fallback rule
185      *     </ul>
186      *   <li>Applies the resolution filter
187      * </ol>
188      * @param resolutionSelector the ResolutionSelector used to sort the candidate
189      *                           sizes.
190      * @param candidateSizes     the candidate sizes after the high resolution processing, which
191      *                           will be sorted by the rule of ResolutionSelector.
192      * @param maxResolution      the max resolutions which sizes larger than it will be removed
193      *                           from candidate sizes.
194      * @param targetRotation     the target rotation to calculate the rotation degrees to the
195      *                           {@link ResolutionFilter}.
196      * @param fullFovRatio       the full FOV's aspect ratio.
197      * @param sensorOrientation  the sensor orientation of the current camera.
198      * @param lensFacing         the lens facing of the current camera
199      * @return a size list which has been filtered and sorted by the specified resolution
200      * selector settings.
201      * @throws IllegalArgumentException if the specified resolution filter returns any size which
202      *                                  is not included in the provided supported size list.
203      */
sortSupportedOutputSizesByResolutionSelector( @onNull ResolutionSelector resolutionSelector, @NonNull List<Size> candidateSizes, @Nullable Size maxResolution, int targetRotation, @NonNull Rational fullFovRatio, int sensorOrientation, int lensFacing)204     public static @NonNull List<Size> sortSupportedOutputSizesByResolutionSelector(
205             @NonNull ResolutionSelector resolutionSelector,
206             @NonNull List<Size> candidateSizes,
207             @Nullable Size maxResolution,
208             int targetRotation,
209             @NonNull Rational fullFovRatio,
210             int sensorOrientation,
211             int lensFacing) {
212 
213         // Applies the aspect ratio strategy onto the resolution candidate list.
214         LinkedHashMap<Rational, List<Size>> aspectRatioSizeListMap =
215                 applyAspectRatioStrategy(candidateSizes,
216                         resolutionSelector.getAspectRatioStrategy(), fullFovRatio);
217 
218         // Applies the max resolution setting
219         if (maxResolution != null) {
220             applyMaxResolutionRestriction(aspectRatioSizeListMap, maxResolution);
221         }
222 
223         // Applies the resolution strategy onto the resolution candidate list.
224         applyResolutionStrategy(aspectRatioSizeListMap, resolutionSelector.getResolutionStrategy());
225 
226         // Collects all sizes from the sorted aspect ratio size groups into the final sorted list.
227         List<Size> resultList = new ArrayList<>();
228         for (List<Size> sortedSizes : aspectRatioSizeListMap.values()) {
229             for (Size size : sortedSizes) {
230                 // A size may exist in multiple groups in mod16 condition. Keep only one in
231                 // the final list.
232                 if (!resultList.contains(size)) {
233                     resultList.add(size);
234                 }
235             }
236         }
237 
238         // Applies the resolution filter onto the resolution candidate list.
239         return applyResolutionFilter(resultList, resolutionSelector.getResolutionFilter(),
240                 targetRotation, sensorOrientation, lensFacing);
241     }
242 
243     /**
244      * Returns the normal supported output sizes.
245      *
246      * <p>When using camera-camera2 implementation, the output sizes are retrieved via
247      * StreamConfigurationMap#getOutputSizes().
248      *
249      * @return the resolution candidate list sorted in descending order.
250      */
getResolutionCandidateList( @ullable List<Pair<Integer, Size[]>> customResolutions, int imageFormat)251     private @NonNull List<Size> getResolutionCandidateList(
252             @Nullable List<Pair<Integer, Size[]>> customResolutions, int imageFormat) {
253         // Tries to get the custom supported resolutions list if it is set
254         List<Size> resolutionCandidateList = getSizeListByFormat(customResolutions, imageFormat);
255 
256         // Tries to get the supported output sizes from the CameraInfoInternal if both custom
257         // ordered and supported resolutions lists are not set.
258         if (resolutionCandidateList == null) {
259             resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
260         }
261 
262         // CameraInfoInternal.getSupportedResolutions is not guaranteed to return a modifiable list
263         // needed by Collections.sort(), so it is converted to a modifiable list here
264         resolutionCandidateList = new ArrayList<>(resolutionCandidateList);
265 
266         Collections.sort(resolutionCandidateList, new CompareSizesByArea(true));
267 
268         if (resolutionCandidateList.isEmpty()) {
269             Logger.w(TAG, "The retrieved supported resolutions from camera info internal is empty"
270                     + ". Format is " + imageFormat + ".");
271         }
272 
273         return resolutionCandidateList;
274     }
275 
276     /**
277      * Appends the high resolution supported output sizes according to the high resolution settings.
278      *
279      * <p>When using camera-camera2 implementation, the output sizes are retrieved via
280      * StreamConfigurationMap#getHighResolutionOutputSizes().
281      *
282      * @param resolutionCandidateList the supported size list which contains only normal output
283      *                                sizes.
284      * @param resolutionSelector      the specified resolution selector.
285      * @param imageFormat             the desired image format for the target use case.
286      * @return the resolution candidate list including the high resolution output sizes sorted in
287      * descending order.
288      */
applyHighResolutionSettings( @onNull List<Size> resolutionCandidateList, @NonNull ResolutionSelector resolutionSelector, int imageFormat)289     private @NonNull List<Size> applyHighResolutionSettings(
290             @NonNull List<Size> resolutionCandidateList,
291             @NonNull ResolutionSelector resolutionSelector, int imageFormat) {
292         // Appends high resolution output sizes if high resolution is enabled by ResolutionSelector
293         if (resolutionSelector.getAllowedResolutionMode()
294                 == ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE) {
295             List<Size> allSizesList = new ArrayList<>();
296             allSizesList.addAll(resolutionCandidateList);
297             allSizesList.addAll(mCameraInfoInternal.getSupportedHighResolutions(imageFormat));
298             Collections.sort(allSizesList, new CompareSizesByArea(true));
299             return allSizesList;
300         }
301 
302         return resolutionCandidateList;
303     }
304 
305     /**
306      * Applies the aspect ratio strategy onto the input resolution candidate list.
307      *
308      * @param resolutionCandidateList the supported sizes list which has been sorted in
309      *                                descending order.
310      * @param aspectRatioStrategy     the specified aspect ratio strategy.
311      * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
312      * is applied and is sorted against the preferred aspect ratio.
313      */
applyAspectRatioStrategy( @onNull List<Size> resolutionCandidateList, @NonNull AspectRatioStrategy aspectRatioStrategy, Rational fullFovRatio)314     private static @NonNull LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategy(
315             @NonNull List<Size> resolutionCandidateList,
316             @NonNull AspectRatioStrategy aspectRatioStrategy,
317             Rational fullFovRatio) {
318         // Group output sizes by aspect ratio.
319         Map<Rational, List<Size>> aspectRatioSizeListMap =
320                 groupSizesByAspectRatio(resolutionCandidateList);
321 
322         // Applies the aspect ratio fallback rule
323         return applyAspectRatioStrategyFallbackRule(
324                 aspectRatioSizeListMap, aspectRatioStrategy, fullFovRatio);
325     }
326 
327     /**
328      * Applies the aspect ratio strategy fallback rule to the aspect ratio to size list map.
329      *
330      * @param sizeGroupsMap       the aspect ratio to size list map. The size list should have been
331      *                            sorted in descending order.
332      * @param aspectRatioStrategy the specified aspect ratio strategy.
333      * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
334      * is applied and is sorted against the preferred aspect ratio.
335      */
applyAspectRatioStrategyFallbackRule( @onNull Map<Rational, List<Size>> sizeGroupsMap, @NonNull AspectRatioStrategy aspectRatioStrategy, Rational fullFovRatio)336     private static LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategyFallbackRule(
337             @NonNull Map<Rational, List<Size>> sizeGroupsMap,
338             @NonNull AspectRatioStrategy aspectRatioStrategy,
339             Rational fullFovRatio) {
340         // Determines the sensor resolution orientation info by the full FOV ratio.
341         boolean isSensorLandscapeResolution = fullFovRatio != null ? fullFovRatio.getNumerator()
342                 >= fullFovRatio.getDenominator() : true;
343         Rational aspectRatio = getTargetAspectRatioRationalValue(
344                 aspectRatioStrategy.getPreferredAspectRatio(), isSensorLandscapeResolution);
345 
346         // Remove items of all other aspect ratios if the fallback rule is AspectRatioStrategy
347         // .FALLBACK_RULE_NONE
348         if (aspectRatioStrategy.getFallbackRule() == AspectRatioStrategy.FALLBACK_RULE_NONE) {
349             Rational preferredAspectRatio = getTargetAspectRatioRationalValue(
350                     aspectRatioStrategy.getPreferredAspectRatio(), isSensorLandscapeResolution);
351             for (Rational ratio : new ArrayList<>(sizeGroupsMap.keySet())) {
352                 if (!ratio.equals(preferredAspectRatio)) {
353                     sizeGroupsMap.remove(ratio);
354                 }
355             }
356         }
357 
358         // Sorts the aspect ratio key set by the preferred aspect ratio.
359         List<Rational> aspectRatios = new ArrayList<>(sizeGroupsMap.keySet());
360         Collections.sort(aspectRatios,
361                 new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
362                         aspectRatio, fullFovRatio));
363 
364         // Stores the size groups into LinkedHashMap to keep the order
365         LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap = new LinkedHashMap<>();
366         for (Rational ratio : aspectRatios) {
367             sortedAspectRatioSizeListMap.put(ratio, sizeGroupsMap.get(ratio));
368         }
369 
370         return sortedAspectRatioSizeListMap;
371     }
372 
373     /**
374      * Applies the resolution strategy onto the aspect ratio to size list linked hash map.
375      *
376      * <p>The resolution fallback rule is applied to filter out and sort the sizes in the
377      * underlying size list.
378      *
379      * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
380      *                                     entries order should not be changed.
381      * @param resolutionStrategy           the resolution strategy to sort the candidate
382      *                                     resolutions.
383      */
applyResolutionStrategy( @onNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap, @Nullable ResolutionStrategy resolutionStrategy)384     private static void applyResolutionStrategy(
385             @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
386             @Nullable ResolutionStrategy resolutionStrategy) {
387         if (resolutionStrategy == null) {
388             return;
389         }
390 
391         // Applies the resolution strategy with the specified fallback rule
392         for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
393             applyResolutionStrategyFallbackRule(sortedAspectRatioSizeListMap.get(key),
394                     resolutionStrategy);
395         }
396     }
397 
398     /**
399      * Applies the resolution strategy fallback rule to the size list.
400      *
401      * @param supportedSizesList the supported sizes list which has been sorted in descending order.
402      * @param resolutionStrategy the resolution strategy to sort the candidate resolutions.
403      */
applyResolutionStrategyFallbackRule( @onNull List<Size> supportedSizesList, @NonNull ResolutionStrategy resolutionStrategy)404     private static void applyResolutionStrategyFallbackRule(
405             @NonNull List<Size> supportedSizesList,
406             @NonNull ResolutionStrategy resolutionStrategy) {
407         if (supportedSizesList.isEmpty()) {
408             return;
409         }
410         Integer fallbackRule = resolutionStrategy.getFallbackRule();
411 
412         if (resolutionStrategy.equals(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)) {
413             // Do nothing for HIGHEST_AVAILABLE_STRATEGY case.
414             return;
415         }
416 
417         Size boundSize = resolutionStrategy.getBoundSize();
418 
419         switch (fallbackRule) {
420             case ResolutionStrategy.FALLBACK_RULE_NONE:
421                 sortSupportedSizesByFallbackRuleNone(supportedSizesList, boundSize);
422                 break;
423             case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER:
424                 sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
425                         boundSize, true);
426                 break;
427             case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER:
428                 sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
429                         boundSize, false);
430                 break;
431             case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER:
432                 sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
433                         boundSize, true);
434                 break;
435             case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER:
436                 sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
437                         boundSize, false);
438                 break;
439             default:
440                 break;
441         }
442     }
443 
444     /**
445      * Applies the max resolution restriction.
446      *
447      * <p>Filters out the output sizes that exceed the max resolution in area size.
448      *
449      * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
450      *                                     entries order should not be changed.
451      * @param maxResolution                the max resolution size.
452      */
applyMaxResolutionRestriction( @onNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap, @NonNull Size maxResolution)453     private static void applyMaxResolutionRestriction(
454             @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
455             @NonNull Size maxResolution) {
456         int maxResolutionAreaSize = SizeUtil.getArea(maxResolution);
457         for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
458             List<Size> supportedSizesList = sortedAspectRatioSizeListMap.get(key);
459             List<Size> filteredResultList = new ArrayList<>();
460             for (Size size : supportedSizesList) {
461                 if (SizeUtil.getArea(size) <= maxResolutionAreaSize) {
462                     filteredResultList.add(size);
463                 }
464             }
465             supportedSizesList.clear();
466             supportedSizesList.addAll(filteredResultList);
467         }
468     }
469 
470     /**
471      * Applies the resolution filtered to the sorted output size list.
472      *
473      * @param sizeList         the supported size list which has been filtered and sorted by the
474      *                         specified aspect ratio, resolution strategies.
475      * @param resolutionFilter the specified resolution filter.
476      * @param targetRotation   the use case target rotation info
477      * @return the result size list applied the specified resolution filter.
478      * @throws IllegalArgumentException if the specified resolution filter returns any size which
479      *                                  is not included in the provided supported size list.
480      */
applyResolutionFilter(@onNull List<Size> sizeList, @Nullable ResolutionFilter resolutionFilter, @ImageOutputConfig.RotationValue int targetRotation, int sensorOrientation, int lensFacing)481     private static @NonNull List<Size> applyResolutionFilter(@NonNull List<Size> sizeList,
482             @Nullable ResolutionFilter resolutionFilter,
483             @ImageOutputConfig.RotationValue int targetRotation,
484             int sensorOrientation,
485             int lensFacing) {
486         if (resolutionFilter == null) {
487             return sizeList;
488         }
489 
490         // Invokes ResolutionFilter#filter() to filter/sort and return the result if it is
491         // specified.
492         int destRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(
493                 targetRotation);
494         int rotationDegrees =
495                 CameraOrientationUtil.getRelativeImageRotation(destRotationDegrees,
496                         sensorOrientation,
497                         lensFacing == CameraSelector.LENS_FACING_BACK);
498         List<Size> filteredResultList = resolutionFilter.filter(new ArrayList<>(sizeList),
499                 rotationDegrees);
500         if (sizeList.containsAll(filteredResultList)) {
501             return filteredResultList;
502         } else {
503             throw new IllegalArgumentException("The returned sizes list of the resolution "
504                     + "filter must be a subset of the provided sizes list.");
505         }
506     }
507 
508     /**
509      * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_NONE}.
510      *
511      * @param supportedSizesList the supported sizes list which has been sorted in descending order.
512      * @param boundSize          the resolution strategy bound size.
513      */
sortSupportedSizesByFallbackRuleNone( @onNull List<Size> supportedSizesList, @NonNull Size boundSize)514     private static void sortSupportedSizesByFallbackRuleNone(
515             @NonNull List<Size> supportedSizesList, @NonNull Size boundSize) {
516         boolean containsBoundSize = supportedSizesList.contains(boundSize);
517         supportedSizesList.clear();
518         if (containsBoundSize) {
519             supportedSizesList.add(boundSize);
520         }
521     }
522 
523     /**
524      * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}
525      * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER}.
526      *
527      * @param supportedSizesList the supported sizes list which has been sorted in descending order.
528      * @param boundSize          the resolution strategy bound size.
529      * @param keepLowerSizes     keeps the sizes lower than the bound size in the result list if
530      *                           this is {@code true}.
531      */
sortSupportedSizesByFallbackRuleClosestHigherThenLower( @onNull List<Size> supportedSizesList, @NonNull Size boundSize, boolean keepLowerSizes)532     static void sortSupportedSizesByFallbackRuleClosestHigherThenLower(
533             @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
534             boolean keepLowerSizes) {
535         List<Size> lowerSizes = new ArrayList<>();
536 
537         for (int i = supportedSizesList.size() - 1; i >= 0; i--) {
538             Size outputSize = supportedSizesList.get(i);
539             if (outputSize.getWidth() < boundSize.getWidth()
540                     || outputSize.getHeight() < boundSize.getHeight()) {
541                 // The supportedSizesList is in descending order. Checking and put the
542                 // bounding-below size at position 0 so that the largest smaller resolution
543                 // will be put in the first position finally.
544                 lowerSizes.add(0, outputSize);
545             } else {
546                 break;
547             }
548         }
549         // Removes the lower sizes from the list
550         supportedSizesList.removeAll(lowerSizes);
551         // Reverses the list so that the smallest larger resolution will be put in the first
552         // position.
553         Collections.reverse(supportedSizesList);
554         if (keepLowerSizes) {
555             // Appends the lower sizes to the tail
556             supportedSizesList.addAll(lowerSizes);
557         }
558     }
559 
560     /**
561      * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER}
562      * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER}.
563      *
564      * @param supportedSizesList the supported sizes list which has been sorted in descending order.
565      * @param boundSize          the resolution strategy bound size.
566      * @param keepHigherSizes    keeps the sizes higher than the bound size in the result list if
567      *                           this is {@code true}.
568      */
sortSupportedSizesByFallbackRuleClosestLowerThenHigher( @onNull List<Size> supportedSizesList, @NonNull Size boundSize, boolean keepHigherSizes)569     private static void sortSupportedSizesByFallbackRuleClosestLowerThenHigher(
570             @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
571             boolean keepHigherSizes) {
572         List<Size> higherSizes = new ArrayList<>();
573 
574         for (int i = 0; i < supportedSizesList.size(); i++) {
575             Size outputSize = supportedSizesList.get(i);
576             if (outputSize.getWidth() > boundSize.getWidth()
577                     || outputSize.getHeight() > boundSize.getHeight()) {
578                 // The supportedSizesList is in descending order. Checking and put the
579                 // bounding-above size at position 0 so that the smallest larger resolution
580                 // will be put in the first position finally.
581                 higherSizes.add(0, outputSize);
582             } else {
583                 // Breaks the for-loop to keep the equal-to or lower sizes in the list.
584                 break;
585             }
586         }
587         // Removes the higher sizes from the list
588         supportedSizesList.removeAll(higherSizes);
589         if (keepHigherSizes) {
590             // Appends the higher sizes to the tail
591             supportedSizesList.addAll(higherSizes);
592         }
593     }
594 
595     /**
596      * Returns the target aspect ratio rational value according to the ResolutionSelector settings.
597      */
getTargetAspectRatioRationalValue(@spectRatio.Ratio int aspectRatio, boolean isSensorLandscapeResolution)598     static @Nullable Rational getTargetAspectRatioRationalValue(@AspectRatio.Ratio int aspectRatio,
599             boolean isSensorLandscapeResolution) {
600         Rational outputRatio = null;
601 
602         switch (aspectRatio) {
603             case AspectRatio.RATIO_4_3:
604                 outputRatio = isSensorLandscapeResolution ? ASPECT_RATIO_4_3
605                         : ASPECT_RATIO_3_4;
606                 break;
607             case AspectRatio.RATIO_16_9:
608                 outputRatio = isSensorLandscapeResolution ? ASPECT_RATIO_16_9
609                         : ASPECT_RATIO_9_16;
610                 break;
611             case AspectRatio.RATIO_DEFAULT:
612                 break;
613             default:
614                 Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
615         }
616 
617         return outputRatio;
618     }
619 
620     /**
621      * Returns the grouping aspect ratio keys of the input resolution list.
622      *
623      * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
624      * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
625      */
getResolutionListGroupingAspectRatioKeys( @onNull List<Size> resolutionCandidateList)626     static @NonNull List<Rational> getResolutionListGroupingAspectRatioKeys(
627             @NonNull List<Size> resolutionCandidateList) {
628         List<Rational> aspectRatios = new ArrayList<>();
629 
630         // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
631         // additional items.
632         aspectRatios.add(ASPECT_RATIO_4_3);
633         aspectRatios.add(ASPECT_RATIO_16_9);
634 
635         // Tries to find the aspect ratio which the target size belongs to.
636         for (Size size : resolutionCandidateList) {
637             Rational newRatio = new Rational(size.getWidth(), size.getHeight());
638             boolean aspectRatioFound = aspectRatios.contains(newRatio);
639 
640             // The checking size might be a mod16 size which can be mapped to an existing aspect
641             // ratio group.
642             if (!aspectRatioFound) {
643                 boolean hasMatchingAspectRatio = false;
644                 for (Rational aspectRatio : aspectRatios) {
645                     if (hasMatchingAspectRatio(size, aspectRatio)) {
646                         hasMatchingAspectRatio = true;
647                         break;
648                     }
649                 }
650                 if (!hasMatchingAspectRatio) {
651                     aspectRatios.add(newRatio);
652                 }
653             }
654         }
655 
656         return aspectRatios;
657     }
658 
659     /**
660      * Groups the input sizes into an aspect ratio to size list map.
661      */
groupSizesByAspectRatio(@onNull List<Size> sizes)662     static Map<Rational, List<Size>> groupSizesByAspectRatio(@NonNull List<Size> sizes) {
663         Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
664 
665         List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
666 
667         for (Rational aspectRatio : aspectRatioKeys) {
668             aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
669         }
670 
671         for (Size outputSize : sizes) {
672             for (Rational key : aspectRatioSizeListMap.keySet()) {
673                 // Put the size into all groups that is matched in mod16 condition since a size
674                 // may match multiple aspect ratio in mod16 algorithm.
675                 if (hasMatchingAspectRatio(outputSize, key)) {
676                     aspectRatioSizeListMap.get(key).add(outputSize);
677                 }
678             }
679         }
680 
681         return aspectRatioSizeListMap;
682     }
683 }
684