/*
|
* 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. <br/>
|
* 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<Size> sizes = new LinkedList<Size>();
|
/**
|
* 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<Size>() {
|
@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<Size> getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera) {
|
List<ResolutionBucket> buckets = parseAvailableSizes(sizes, isBackCamera);
|
|
List<Float> sortedDesiredAspectRatios = new ArrayList<Float>();
|
// 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<Size> result = new ArrayList<Size>(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<Size> pickUpToThree(List<Size> sizes) {
|
List<Size> result = new ArrayList<Size>();
|
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<ResolutionBucket> parseAvailableSizes(List<Size> sizes, boolean isBackCamera) {
|
HashMap<Float, ResolutionBucket> aspectRatioToBuckets = new HashMap<Float, ResolutionBucket>();
|
|
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<ResolutionBucket> sortedBuckets = new ArrayList<ResolutionBucket>(
|
aspectRatioToBuckets.values());
|
Collections.sort(sortedBuckets, new Comparator<ResolutionBucket>() {
|
@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<Size> 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<Size> filterBlackListedSizes(List<Size> sizes, String blacklistString) {
|
String[] blacklistStringArray = blacklistString.split(",");
|
if (blacklistStringArray.length == 0) {
|
return sizes;
|
}
|
|
Set<String> blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray));
|
List<Size> 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<String> blacklistedSizes = new HashSet(Lists.newArrayList(blacklistStringArray));
|
return isBlackListed(size, blacklistedSizes);
|
}
|
|
private static boolean isBlackListed(@Nonnull Size size, @Nonnull Set<String> blacklistedSizes) {
|
String sizeStr = size.getWidth() + "x" + size.getHeight();
|
return blacklistedSizes.contains(sizeStr);
|
}
|
}
|