Home | History | Annotate | Download | only in settings
      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          */
    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