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