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