/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera.settings; import android.content.Context; import android.util.DisplayMetrics; import android.view.WindowManager; import com.android.camera.exif.Rational; import com.android.camera.util.AndroidServices; import com.android.camera.util.ApiHelper; import com.android.camera.util.Size; import com.google.common.collect.Lists; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; /** * This class is used to help manage the many different resolutions available on * the device.
* It allows you to specify which aspect ratios to offer the user, and then * chooses which resolutions are the most pertinent to avoid overloading the * user with so many options. */ public class ResolutionUtil { /** * Different aspect ratio constants. */ public static final Rational ASPECT_RATIO_16x9 = new Rational(16, 9); public static final Rational ASPECT_RATIO_4x3 = new Rational(4, 3); private static final double ASPECT_RATIO_TOLERANCE = 0.05; public static final String NEXUS_5_LARGE_16_BY_9 = "1836x3264"; public static final float NEXUS_5_LARGE_16_BY_9_ASPECT_RATIO = 16f / 9f; public static Size NEXUS_5_LARGE_16_BY_9_SIZE = new Size(3264, 1836); /** * These are the preferred aspect ratios for the settings. We will take HAL * supported aspect ratios that are within ASPECT_RATIO_TOLERANCE of these values. * We will also take the maximum supported resolution for full sensor image. */ private static Float[] sDesiredAspectRatios = { 16.0f / 9.0f, 4.0f / 3.0f }; private static Size[] sDesiredAspectRatioSizes = { new Size(16, 9), new Size(4, 3) }; /** * A resolution bucket holds a list of sizes that are of a given aspect * ratio. */ private static class ResolutionBucket { public Float aspectRatio; /** * This is a sorted list of sizes, going from largest to smallest. */ public List sizes = new LinkedList(); /** * This is the head of the sizes array. */ public Size largest; /** * This is the area of the largest size, used for sorting * ResolutionBuckets. */ public Integer maxPixels = 0; /** * Use this to add a new resolution to this bucket. It will insert it * into the sizes array and update appropriate members. * * @param size the new size to be added */ public void add(Size size) { sizes.add(size); Collections.sort(sizes, new Comparator() { @Override public int compare(Size size, Size size2) { // sort area greatest to least return Integer.compare(size2.width() * size2.height(), size.width() * size.height()); } }); maxPixels = sizes.get(0).width() * sizes.get(0).height(); } } /** * Given a list of camera sizes, this uses some heuristics to decide which * options to present to a user. It currently returns up to 3 sizes for each * aspect ratio. The aspect ratios returned include the ones in * sDesiredAspectRatios, and the largest full sensor ratio. T his guarantees * that users can use a full-sensor size, as well as any of the preferred * aspect ratios from above; * * @param sizes A super set of all sizes to be displayed * @param isBackCamera true if these are sizes for the back camera * @return The list of sizes to display grouped first by aspect ratio * (sorted by maximum area), and sorted within aspect ratio by area) */ public static List getDisplayableSizesFromSupported(List sizes, boolean isBackCamera) { List buckets = parseAvailableSizes(sizes, isBackCamera); List sortedDesiredAspectRatios = new ArrayList(); // We want to make sure we support the maximum pixel aspect ratio, even // if it doesn't match a desired aspect ratio sortedDesiredAspectRatios.add(buckets.get(0).aspectRatio.floatValue()); // Now go through the buckets from largest mp to smallest, adding // desired ratios for (ResolutionBucket bucket : buckets) { Float aspectRatio = bucket.aspectRatio; if (Arrays.asList(sDesiredAspectRatios).contains(aspectRatio) && !sortedDesiredAspectRatios.contains(aspectRatio)) { sortedDesiredAspectRatios.add(aspectRatio); } } List result = new ArrayList(sizes.size()); for (Float targetRatio : sortedDesiredAspectRatios) { for (ResolutionBucket bucket : buckets) { Number aspectRatio = bucket.aspectRatio; if (Math.abs(aspectRatio.floatValue() - targetRatio) <= ASPECT_RATIO_TOLERANCE) { result.addAll(pickUpToThree(bucket.sizes)); } } } return result; } /** * Get the area in pixels of a size. * * @param size the size to measure * @return the area. */ private static int area(Size size) { if (size == null) { return 0; } return size.width() * size.height(); } /** * Given a list of sizes of a similar aspect ratio, it tries to pick evenly * spaced out options. It starts with the largest, then tries to find one at * 50% of the last chosen size for the subsequent size. * * @param sizes A list of Sizes that are all of a similar aspect ratio * @return A list of at least one, and no more than three representative * sizes from the list. */ private static List pickUpToThree(List sizes) { List result = new ArrayList(); Size largest = sizes.get(0); result.add(largest); Size lastSize = largest; for (Size size : sizes) { double targetArea = Math.pow(.5, result.size()) * area(largest); if (area(size) < targetArea) { // This candidate is smaller than half the mega pixels of the // last one. Let's see whether the previous size, or this size // is closer to the desired target. if (!result.contains(lastSize) && (targetArea - area(lastSize) < area(size) - targetArea)) { result.add(lastSize); } else { result.add(size); } } lastSize = size; if (result.size() == 3) { break; } } // If we have less than three, we can add the smallest size. if (result.size() < 3 && !result.contains(lastSize)) { result.add(lastSize); } return result; } /** * Take an aspect ratio and squish it into a nearby desired aspect ratio, if * possible. * * @param aspectRatio the aspect ratio to fuzz * @return the closest desiredAspectRatio within ASPECT_RATIO_TOLERANCE, or the * original ratio */ private static float fuzzAspectRatio(float aspectRatio) { for (float desiredAspectRatio : sDesiredAspectRatios) { if ((Math.abs(aspectRatio - desiredAspectRatio)) < ASPECT_RATIO_TOLERANCE) { return desiredAspectRatio; } } return aspectRatio; } /** * This takes a bunch of supported sizes and buckets them by aspect ratio. * The result is a list of buckets sorted by each bucket's largest area. * They are sorted from largest to smallest. This will bucket aspect ratios * that are close to the sDesiredAspectRatios in to the same bucket. * * @param sizes all supported sizes for a camera * @param isBackCamera true if these are sizes for the back camera * @return all of the sizes grouped by their closest aspect ratio */ private static List parseAvailableSizes(List sizes, boolean isBackCamera) { HashMap aspectRatioToBuckets = new HashMap(); for (Size size : sizes) { Float aspectRatio = (float) size.getWidth() / (float) size.getHeight(); // If this aspect ratio is close to a desired Aspect Ratio, // fuzz it so that they are bucketed together aspectRatio = fuzzAspectRatio(aspectRatio); ResolutionBucket bucket = aspectRatioToBuckets.get(aspectRatio); if (bucket == null) { bucket = new ResolutionBucket(); bucket.aspectRatio = aspectRatio; aspectRatioToBuckets.put(aspectRatio, bucket); } bucket.add(size); } if (ApiHelper.IS_NEXUS_5 && isBackCamera) { aspectRatioToBuckets.get(16 / 9.0f).add(NEXUS_5_LARGE_16_BY_9_SIZE); } List sortedBuckets = new ArrayList( aspectRatioToBuckets.values()); Collections.sort(sortedBuckets, new Comparator() { @Override public int compare(ResolutionBucket resolutionBucket, ResolutionBucket resolutionBucket2) { return Integer.compare(resolutionBucket2.maxPixels, resolutionBucket.maxPixels); } }); return sortedBuckets; } /** * Given a size, return a string describing the aspect ratio by reducing the * * @param size the size to describe * @return a string description of the aspect ratio */ public static String aspectRatioDescription(Size size) { Size aspectRatio = reduce(size); return aspectRatio.width() + "x" + aspectRatio.height(); } /** * Reduce an aspect ratio to its lowest common denominator. The ratio of the * input and output sizes is guaranteed to be the same. * * @param aspectRatio the aspect ratio to reduce * @return The reduced aspect ratio which may equal the original. */ public static Size reduce(Size aspectRatio) { BigInteger width = BigInteger.valueOf(aspectRatio.width()); BigInteger height = BigInteger.valueOf(aspectRatio.height()); BigInteger gcd = width.gcd(height); int numerator = Math.max(width.intValue(), height.intValue()) / gcd.intValue(); int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); return new Size(numerator, denominator); } /** * Given a size return the numerator of its aspect ratio * * @param size the size to measure * @return the numerator */ public static int aspectRatioNumerator(Size size) { Size aspectRatio = reduce(size); return aspectRatio.width(); } /** * Given a size, return the closest aspect ratio that falls close to the * given size. * * @param size the size to approximate * @return the closest desired aspect ratio, or the original aspect ratio if * none were close enough */ public static Size getApproximateSize(Size size) { Size aspectRatio = reduce(size); float fuzzy = fuzzAspectRatio(size.width() / (float) size.height()); int index = Arrays.asList(sDesiredAspectRatios).indexOf(fuzzy); if (index != -1) { aspectRatio = sDesiredAspectRatioSizes[index]; } return aspectRatio; } /** * Given a size return the numerator of its aspect ratio * * @param size * @return the denominator */ public static int aspectRatioDenominator(Size size) { BigInteger width = BigInteger.valueOf(size.width()); BigInteger height = BigInteger.valueOf(size.height()); BigInteger gcd = width.gcd(height); int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); return denominator; } /** * Returns the aspect ratio for the given size. * * @param size The given size. * @return A {@link Rational} which represents the aspect ratio. */ public static Rational getAspectRatio(Size size) { int width = size.getWidth(); int height = size.getHeight(); int numerator = width; int denominator = height; if (height > width) { numerator = height; denominator = width; } return new Rational(numerator, denominator); } public static boolean hasSameAspectRatio(Rational ar1, Rational ar2) { return Math.abs(ar1.toDouble() - ar2.toDouble()) < ASPECT_RATIO_TOLERANCE; } /** * Selects the maximal resolution for the given desired aspect ratio from all available * resolutions. If no resolution exists for the desired aspect ratio, return a resolution * with the maximum number of pixels. * * @param desiredAspectRatio The desired aspect ratio. * @param sizes All available resolutions. * @return The maximal resolution for desired aspect ratio ; if no sizes are found, then * return size of (0,0) */ public static Size getLargestPictureSize(Rational desiredAspectRatio, List sizes) { int maxPixelNumNoAspect = 0; Size maxSize = new Size(0, 0); // Fix for b/21758681 // Do first pass with the candidate with closest size, regardless of aspect ratio, // to loosen the requirement of valid preview sizes. As long as one size exists // in the list, we should pass back a valid size. for (Size size : sizes) { int pixelNum = size.getWidth() * size.getHeight(); if (pixelNum > maxPixelNumNoAspect) { maxPixelNumNoAspect = pixelNum; maxSize = size; } } // With second pass, override first pass with the candidate with closest // size AND similar aspect ratio. If there are no valid candidates are found // in the second pass, take the candidate from the first pass. int maxPixelNumWithAspect = 0; for (Size size : sizes) { Rational aspectRatio = getAspectRatio(size); // Skip if the aspect ratio is not desired. if (!hasSameAspectRatio(aspectRatio, desiredAspectRatio)) { continue; } int pixelNum = size.getWidth() * size.getHeight(); if (pixelNum > maxPixelNumWithAspect) { maxPixelNumWithAspect = pixelNum; maxSize = size; } } return maxSize; } public static DisplayMetrics getDisplayMetrics(Context context) { DisplayMetrics displayMetrics = new DisplayMetrics(); WindowManager wm = AndroidServices.instance().provideWindowManager(); if (wm != null) { wm.getDefaultDisplay().getMetrics(displayMetrics); } return displayMetrics; } /** * Takes selected sizes and a list of blacklisted sizes. All the blacklistes * sizes will be removed from the 'sizes' list. * * @param sizes the sizes to be filtered. * @param blacklistString a String containing a comma-separated list of * sizes that should be removed from the original list. * @return A list that contains the filtered items. */ @ParametersAreNonnullByDefault public static List filterBlackListedSizes(List sizes, String blacklistString) { String[] blacklistStringArray = blacklistString.split(","); if (blacklistStringArray.length == 0) { return sizes; } Set blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray)); List newSizeList = new ArrayList<>(); for (Size size : sizes) { if (!isBlackListed(size, blacklistedSizes)) { newSizeList.add(size); } } return newSizeList; } /** * Returns whether the given size is within the blacklist string. * * @param size the size to check * @param blacklistString a String containing a comma-separated list of * sizes that should not be available on the device. * @return Whether the given size is blacklisted. */ public static boolean isBlackListed(@Nonnull Size size, @Nonnull String blacklistString) { String[] blacklistStringArray = blacklistString.split(","); if (blacklistStringArray.length == 0) { return false; } Set blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray)); return isBlackListed(size, blacklistedSizes); } private static boolean isBlackListed(@Nonnull Size size, @Nonnull Set blacklistedSizes) { String sizeStr = size.getWidth() + "x" + size.getHeight(); return blacklistedSizes.contains(sizeStr); } }