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
17package com.android.camera.settings;
18
19import android.content.Context;
20import android.util.DisplayMetrics;
21import android.view.WindowManager;
22
23import com.android.camera.exif.Rational;
24import com.android.camera.util.AndroidServices;
25import com.android.camera.util.ApiHelper;
26import com.android.camera.util.Size;
27
28import com.google.common.collect.Lists;
29
30import java.math.BigInteger;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Collections;
34import java.util.Comparator;
35import java.util.HashMap;
36import java.util.HashSet;
37import java.util.LinkedList;
38import java.util.List;
39import java.util.Set;
40
41import javax.annotation.Nonnull;
42import 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 */
52public 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         */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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
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 blacklisted sizes. All the blacklistes
419     * sizes will be removed from the 'sizes' list.
420     *
421     * @param sizes the sizes to be filtered.
422     * @param blacklistString 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
427    public static List<Size> filterBlackListedSizes(List<Size> sizes, String blacklistString) {
428        String[] blacklistStringArray = blacklistString.split(",");
429        if (blacklistStringArray.length == 0) {
430            return sizes;
431        }
432
433        Set<String> blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray));
434        List<Size> newSizeList = new ArrayList<>();
435        for (Size size : sizes) {
436            if (!isBlackListed(size, blacklistedSizes)) {
437                newSizeList.add(size);
438            }
439        }
440        return newSizeList;
441    }
442
443    /**
444     * Returns whether the given size is within the blacklist string.
445     *
446     * @param size the size to check
447     * @param blacklistString a String containing a comma-separated list of
448     *            sizes that should not be available on the device.
449     * @return Whether the given size is blacklisted.
450     */
451    public static boolean isBlackListed(@Nonnull Size size, @Nonnull String blacklistString) {
452        String[] blacklistStringArray = blacklistString.split(",");
453        if (blacklistStringArray.length == 0) {
454            return false;
455        }
456        Set<String> blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray));
457        return isBlackListed(size, blacklistedSizes);
458    }
459
460    private static boolean isBlackListed(@Nonnull Size size, @Nonnull Set<String> blacklistedSizes) {
461        String sizeStr = size.getWidth() + "x" + size.getHeight();
462        return blacklistedSizes.contains(sizeStr);
463    }
464}
465