1 /*
2  * Copyright (C) 2013 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 com.android.camera.settings;
18 
19 import android.content.Context;
20 import android.util.DisplayMetrics;
21 import android.view.WindowManager;
22 
23 import com.android.camera.exif.Rational;
24 import com.android.camera.util.AndroidServices;
25 import com.android.camera.util.ApiHelper;
26 import com.android.camera.util.Size;
27 
28 import com.google.common.collect.Lists;
29 
30 import java.math.BigInteger;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Set;
40 
41 import javax.annotation.Nonnull;
42 import javax.annotation.ParametersAreNonnullByDefault;
43 
44 
45 /**
46  * This class is used to help manage the many different resolutions available on
47  * the device. <br/>
48  * It allows you to specify which aspect ratios to offer the user, and then
49  * chooses which resolutions are the most pertinent to avoid overloading the
50  * user with so many options.
51  */
52 public class ResolutionUtil {
53     /**
54      * Different aspect ratio constants.
55      */
56     public static final Rational ASPECT_RATIO_16x9 = new Rational(16, 9);
57     public static final Rational ASPECT_RATIO_4x3 = new Rational(4, 3);
58     private static final double ASPECT_RATIO_TOLERANCE = 0.05;
59 
60     public static final String NEXUS_5_LARGE_16_BY_9 = "1836x3264";
61     public static final float NEXUS_5_LARGE_16_BY_9_ASPECT_RATIO = 16f / 9f;
62     public static Size NEXUS_5_LARGE_16_BY_9_SIZE = new Size(3264, 1836);
63 
64     /**
65      * These are the preferred aspect ratios for the settings. We will take HAL
66      * supported aspect ratios that are within ASPECT_RATIO_TOLERANCE of these values.
67      * We will also take the maximum supported resolution for full sensor image.
68      */
69     private static Float[] sDesiredAspectRatios = {
70             16.0f / 9.0f, 4.0f / 3.0f
71     };
72 
73     private static Size[] sDesiredAspectRatioSizes = {
74             new Size(16, 9), new Size(4, 3)
75     };
76 
77     /**
78      * A resolution bucket holds a list of sizes that are of a given aspect
79      * ratio.
80      */
81     private static class ResolutionBucket {
82         public Float aspectRatio;
83         /**
84          * This is a sorted list of sizes, going from largest to smallest.
85          */
86         public List<Size> sizes = new LinkedList<Size>();
87         /**
88          * This is the head of the sizes array.
89          */
90         public Size largest;
91         /**
92          * This is the area of the largest size, used for sorting
93          * ResolutionBuckets.
94          */
95         public Integer maxPixels = 0;
96 
97         /**
98          * Use this to add a new resolution to this bucket. It will insert it
99          * into the sizes array and update appropriate members.
100          *
101          * @param size the new size to be added
102          */
add(Size size)103         public void add(Size size) {
104             sizes.add(size);
105             Collections.sort(sizes, new Comparator<Size>() {
106                 @Override
107                 public int compare(Size size, Size size2) {
108                     // sort area greatest to least
109                     return Integer.compare(size2.width() * size2.height(),
110                             size.width() * size.height());
111                 }
112             });
113             maxPixels = sizes.get(0).width() * sizes.get(0).height();
114         }
115     }
116 
117     /**
118      * Given a list of camera sizes, this uses some heuristics to decide which
119      * options to present to a user. It currently returns up to 3 sizes for each
120      * aspect ratio. The aspect ratios returned include the ones in
121      * sDesiredAspectRatios, and the largest full sensor ratio. T his guarantees
122      * that users can use a full-sensor size, as well as any of the preferred
123      * aspect ratios from above;
124      *
125      * @param sizes A super set of all sizes to be displayed
126      * @param isBackCamera true if these are sizes for the back camera
127      * @return The list of sizes to display grouped first by aspect ratio
128      *         (sorted by maximum area), and sorted within aspect ratio by area)
129      */
getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera)130     public static List<Size> getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera) {
131         List<ResolutionBucket> buckets = parseAvailableSizes(sizes, isBackCamera);
132 
133         List<Float> sortedDesiredAspectRatios = new ArrayList<Float>();
134         // We want to make sure we support the maximum pixel aspect ratio, even
135         // if it doesn't match a desired aspect ratio
136         sortedDesiredAspectRatios.add(buckets.get(0).aspectRatio.floatValue());
137 
138         // Now go through the buckets from largest mp to smallest, adding
139         // desired ratios
140         for (ResolutionBucket bucket : buckets) {
141             Float aspectRatio = bucket.aspectRatio;
142             if (Arrays.asList(sDesiredAspectRatios).contains(aspectRatio)
143                     && !sortedDesiredAspectRatios.contains(aspectRatio)) {
144                 sortedDesiredAspectRatios.add(aspectRatio);
145             }
146         }
147 
148         List<Size> result = new ArrayList<Size>(sizes.size());
149         for (Float targetRatio : sortedDesiredAspectRatios) {
150             for (ResolutionBucket bucket : buckets) {
151                 Number aspectRatio = bucket.aspectRatio;
152                 if (Math.abs(aspectRatio.floatValue() - targetRatio) <= ASPECT_RATIO_TOLERANCE) {
153                     result.addAll(pickUpToThree(bucket.sizes));
154                 }
155             }
156         }
157         return result;
158     }
159 
160     /**
161      * Get the area in pixels of a size.
162      *
163      * @param size the size to measure
164      * @return the area.
165      */
area(Size size)166     private static int area(Size size) {
167         if (size == null) {
168             return 0;
169         }
170         return size.width() * size.height();
171     }
172 
173     /**
174      * Given a list of sizes of a similar aspect ratio, it tries to pick evenly
175      * spaced out options. It starts with the largest, then tries to find one at
176      * 50% of the last chosen size for the subsequent size.
177      *
178      * @param sizes A list of Sizes that are all of a similar aspect ratio
179      * @return A list of at least one, and no more than three representative
180      *         sizes from the list.
181      */
pickUpToThree(List<Size> sizes)182     private static List<Size> pickUpToThree(List<Size> sizes) {
183         List<Size> result = new ArrayList<Size>();
184         Size largest = sizes.get(0);
185         result.add(largest);
186         Size lastSize = largest;
187         for (Size size : sizes) {
188             double targetArea = Math.pow(.5, result.size()) * area(largest);
189             if (area(size) < targetArea) {
190                 // This candidate is smaller than half the mega pixels of the
191                 // last one. Let's see whether the previous size, or this size
192                 // is closer to the desired target.
193                 if (!result.contains(lastSize)
194                         && (targetArea - area(lastSize) < area(size) - targetArea)) {
195                     result.add(lastSize);
196                 } else {
197                     result.add(size);
198                 }
199             }
200             lastSize = size;
201             if (result.size() == 3) {
202                 break;
203             }
204         }
205 
206         // If we have less than three, we can add the smallest size.
207         if (result.size() < 3 && !result.contains(lastSize)) {
208             result.add(lastSize);
209         }
210         return result;
211     }
212 
213     /**
214      * Take an aspect ratio and squish it into a nearby desired aspect ratio, if
215      * possible.
216      *
217      * @param aspectRatio the aspect ratio to fuzz
218      * @return the closest desiredAspectRatio within ASPECT_RATIO_TOLERANCE, or the
219      *         original ratio
220      */
fuzzAspectRatio(float aspectRatio)221     private static float fuzzAspectRatio(float aspectRatio) {
222         for (float desiredAspectRatio : sDesiredAspectRatios) {
223             if ((Math.abs(aspectRatio - desiredAspectRatio)) < ASPECT_RATIO_TOLERANCE) {
224                 return desiredAspectRatio;
225             }
226         }
227         return aspectRatio;
228     }
229 
230     /**
231      * This takes a bunch of supported sizes and buckets them by aspect ratio.
232      * The result is a list of buckets sorted by each bucket's largest area.
233      * They are sorted from largest to smallest. This will bucket aspect ratios
234      * that are close to the sDesiredAspectRatios in to the same bucket.
235      *
236      * @param sizes all supported sizes for a camera
237      * @param isBackCamera true if these are sizes for the back camera
238      * @return all of the sizes grouped by their closest aspect ratio
239      */
parseAvailableSizes(List<Size> sizes, boolean isBackCamera)240     private static List<ResolutionBucket> parseAvailableSizes(List<Size> sizes, boolean isBackCamera) {
241         HashMap<Float, ResolutionBucket> aspectRatioToBuckets = new HashMap<Float, ResolutionBucket>();
242 
243         for (Size size : sizes) {
244             Float aspectRatio = (float) size.getWidth() / (float) size.getHeight();
245             // If this aspect ratio is close to a desired Aspect Ratio,
246             // fuzz it so that they are bucketed together
247             aspectRatio = fuzzAspectRatio(aspectRatio);
248             ResolutionBucket bucket = aspectRatioToBuckets.get(aspectRatio);
249             if (bucket == null) {
250                 bucket = new ResolutionBucket();
251                 bucket.aspectRatio = aspectRatio;
252                 aspectRatioToBuckets.put(aspectRatio, bucket);
253             }
254             bucket.add(size);
255         }
256         if (ApiHelper.IS_NEXUS_5 && isBackCamera) {
257             aspectRatioToBuckets.get(16 / 9.0f).add(NEXUS_5_LARGE_16_BY_9_SIZE);
258         }
259         List<ResolutionBucket> sortedBuckets = new ArrayList<ResolutionBucket>(
260                 aspectRatioToBuckets.values());
261         Collections.sort(sortedBuckets, new Comparator<ResolutionBucket>() {
262             @Override
263             public int compare(ResolutionBucket resolutionBucket, ResolutionBucket resolutionBucket2) {
264                 return Integer.compare(resolutionBucket2.maxPixels, resolutionBucket.maxPixels);
265             }
266         });
267         return sortedBuckets;
268     }
269 
270     /**
271      * Given a size, return a string describing the aspect ratio by reducing the
272      *
273      * @param size the size to describe
274      * @return a string description of the aspect ratio
275      */
aspectRatioDescription(Size size)276     public static String aspectRatioDescription(Size size) {
277         Size aspectRatio = reduce(size);
278         return aspectRatio.width() + "x" + aspectRatio.height();
279     }
280 
281     /**
282      * Reduce an aspect ratio to its lowest common denominator. The ratio of the
283      * input and output sizes is guaranteed to be the same.
284      *
285      * @param aspectRatio the aspect ratio to reduce
286      * @return The reduced aspect ratio which may equal the original.
287      */
reduce(Size aspectRatio)288     public static Size reduce(Size aspectRatio) {
289         BigInteger width = BigInteger.valueOf(aspectRatio.width());
290         BigInteger height = BigInteger.valueOf(aspectRatio.height());
291         BigInteger gcd = width.gcd(height);
292         int numerator = Math.max(width.intValue(), height.intValue()) / gcd.intValue();
293         int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue();
294         return new Size(numerator, denominator);
295     }
296 
297     /**
298      * Given a size return the numerator of its aspect ratio
299      *
300      * @param size the size to measure
301      * @return the numerator
302      */
aspectRatioNumerator(Size size)303     public static int aspectRatioNumerator(Size size) {
304         Size aspectRatio = reduce(size);
305         return aspectRatio.width();
306     }
307 
308     /**
309      * Given a size, return the closest aspect ratio that falls close to the
310      * given size.
311      *
312      * @param size the size to approximate
313      * @return the closest desired aspect ratio, or the original aspect ratio if
314      *         none were close enough
315      */
getApproximateSize(Size size)316     public static Size getApproximateSize(Size size) {
317         Size aspectRatio = reduce(size);
318         float fuzzy = fuzzAspectRatio(size.width() / (float) size.height());
319         int index = Arrays.asList(sDesiredAspectRatios).indexOf(fuzzy);
320         if (index != -1) {
321             aspectRatio = sDesiredAspectRatioSizes[index];
322         }
323         return aspectRatio;
324     }
325 
326     /**
327      * Given a size return the numerator of its aspect ratio
328      *
329      * @param size
330      * @return the denominator
331      */
aspectRatioDenominator(Size size)332     public static int aspectRatioDenominator(Size size) {
333         BigInteger width = BigInteger.valueOf(size.width());
334         BigInteger height = BigInteger.valueOf(size.height());
335         BigInteger gcd = width.gcd(height);
336         int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue();
337         return denominator;
338     }
339 
340     /**
341      * Returns the aspect ratio for the given size.
342      *
343      * @param size The given size.
344      * @return A {@link Rational} which represents the aspect ratio.
345      */
getAspectRatio(Size size)346     public static Rational getAspectRatio(Size size) {
347         int width = size.getWidth();
348         int height = size.getHeight();
349         int numerator = width;
350         int denominator = height;
351         if (height > width) {
352             numerator = height;
353             denominator = width;
354         }
355         return new Rational(numerator, denominator);
356     }
357 
hasSameAspectRatio(Rational ar1, Rational ar2)358     public static boolean hasSameAspectRatio(Rational ar1, Rational ar2) {
359         return Math.abs(ar1.toDouble() - ar2.toDouble()) < ASPECT_RATIO_TOLERANCE;
360     }
361 
362     /**
363      * Selects the maximal resolution for the given desired aspect ratio from all available
364      * resolutions.  If no resolution exists for the desired aspect ratio, return a resolution
365      * with the maximum number of pixels.
366      *
367      * @param desiredAspectRatio The desired aspect ratio.
368      * @param sizes All available resolutions.
369      * @return The maximal resolution for desired aspect ratio ; if no sizes are found, then
370      *      return size of (0,0)
371      */
getLargestPictureSize(Rational desiredAspectRatio, List<Size> sizes)372     public static Size getLargestPictureSize(Rational desiredAspectRatio, List<Size> sizes) {
373         int maxPixelNumNoAspect = 0;
374         Size maxSize = new Size(0, 0);
375 
376         // Fix for b/21758681
377         // Do first pass with the candidate with closest size, regardless of aspect ratio,
378         // to loosen the requirement of valid preview sizes.  As long as one size exists
379         // in the list, we should pass back a valid size.
380         for (Size size : sizes) {
381             int pixelNum = size.getWidth() * size.getHeight();
382             if (pixelNum > maxPixelNumNoAspect) {
383                 maxPixelNumNoAspect = pixelNum;
384                 maxSize = size;
385             }
386         }
387 
388         // With second pass, override first pass with the candidate with closest
389         // size AND similar aspect ratio.  If there are no valid candidates are found
390         // in the second pass, take the candidate from the first pass.
391         int maxPixelNumWithAspect = 0;
392         for (Size size : sizes) {
393             Rational aspectRatio = getAspectRatio(size);
394             // Skip if the aspect ratio is not desired.
395             if (!hasSameAspectRatio(aspectRatio, desiredAspectRatio)) {
396                 continue;
397             }
398             int pixelNum = size.getWidth() * size.getHeight();
399             if (pixelNum > maxPixelNumWithAspect) {
400                 maxPixelNumWithAspect = pixelNum;
401                 maxSize = size;
402             }
403         }
404 
405         return maxSize;
406     }
407 
getDisplayMetrics(Context context)408     public static DisplayMetrics getDisplayMetrics(Context context) {
409         DisplayMetrics displayMetrics = new DisplayMetrics();
410         WindowManager wm = AndroidServices.instance().provideWindowManager();
411         if (wm != null) {
412             wm.getDefaultDisplay().getMetrics(displayMetrics);
413         }
414         return displayMetrics;
415     }
416 
417     /**
418      * Takes selected sizes and a list of disallowedlisted sizes. All the disallowedlistes
419      * sizes will be removed from the 'sizes' list.
420      *
421      * @param sizes the sizes to be filtered.
422      * @param disallowedlistString a String containing a comma-separated list of
423      *            sizes that should be removed from the original list.
424      * @return A list that contains the filtered items.
425      */
426     @ParametersAreNonnullByDefault
filterDisallowedListedSizes(List<Size> sizes, String disallowedlistString)427     public static List<Size> filterDisallowedListedSizes(List<Size> sizes,
428             String disallowedlistString) {
429         String[] disallowedlistStringArray = disallowedlistString.split(",");
430         if (disallowedlistStringArray.length == 0) {
431             return sizes;
432         }
433 
434         Set<String> disallowedlistedSizes = new HashSet(Lists.newArrayList(
435                 disallowedlistStringArray));
436         List<Size> newSizeList = new ArrayList<>();
437         for (Size size : sizes) {
438             if (!isDisallowedListed(size, disallowedlistedSizes)) {
439                 newSizeList.add(size);
440             }
441         }
442         return newSizeList;
443     }
444 
445     /**
446      * Returns whether the given size is within the disallowedlist string.
447      *
448      * @param size the size to check
449      * @param disallowedlistString a String containing a comma-separated list of
450      *            sizes that should not be available on the device.
451      * @return Whether the given size is disallowedlisted.
452      */
isDisallowedListed(@onnull Size size, @Nonnull String disallowedlistString)453     public static boolean isDisallowedListed(@Nonnull Size size,
454             @Nonnull String disallowedlistString) {
455         String[] disallowedlistStringArray = disallowedlistString.split(",");
456         if (disallowedlistStringArray.length == 0) {
457             return false;
458         }
459         Set<String> disallowedlistedSizes = new HashSet(Lists.newArrayList(
460                 disallowedlistStringArray));
461         return isDisallowedListed(size, disallowedlistedSizes);
462     }
463 
isDisallowedListed(@onnull Size size, @Nonnull Set<String> disallowedlistedSizes)464     private static boolean isDisallowedListed(@Nonnull Size size,
465             @Nonnull Set<String> disallowedlistedSizes) {
466         String sizeStr = size.getWidth() + "x" + size.getHeight();
467         return disallowedlistedSizes.contains(sizeStr);
468     }
469 }
470