Allow multiple aspect ratios to be set for the
resolution setting.

Bug: 13328191
Change-Id: Ie067d18b70bc1ae84dc284f881b7f6030f6a6622
diff --git a/src/com/android/camera/settings/CameraSettingsActivity.java b/src/com/android/camera/settings/CameraSettingsActivity.java
index f1e0ec4..4f1b0af 100644
--- a/src/com/android/camera/settings/CameraSettingsActivity.java
+++ b/src/com/android/camera/settings/CameraSettingsActivity.java
@@ -84,8 +84,10 @@
         private String[] mCamcorderProfileNames;
 
         // Selected resolutions for the different cameras and sizes.
-        private SelectedPictureSizes mPictureSizesBack;
-        private SelectedPictureSizes mPictureSizesFront;
+        private SelectedPictureSizes mOldPictureSizesBack;
+        private SelectedPictureSizes mOldPictureSizesFront;
+        private List<Size> mPictureSizesBack;
+        private List<Size> mPictureSizesFront;
         private SelectedVideoQualities mVideoQualitiesBack;
         private SelectedVideoQualities mVideoQualitiesFront;
 
@@ -284,9 +286,9 @@
 
             ListPreference listPreference = (ListPreference) preference;
             if (listPreference.getKey().equals(SettingsManager.KEY_PICTURE_SIZE_BACK)) {
-                setSummaryForSelection(mPictureSizesBack, listPreference);
+                setSummaryForSelection(mOldPictureSizesBack, mPictureSizesBack, listPreference);
             } else if (listPreference.getKey().equals(SettingsManager.KEY_PICTURE_SIZE_FRONT)) {
-                setSummaryForSelection(mPictureSizesFront, listPreference);
+                setSummaryForSelection(mOldPictureSizesFront, mPictureSizesFront, listPreference);
             } else if (listPreference.getKey().equals(SettingsManager.KEY_VIDEO_QUALITY_BACK)) {
                 setSummaryForSelection(mVideoQualitiesBack, listPreference);
             } else if (listPreference.getKey().equals(SettingsManager.KEY_VIDEO_QUALITY_FRONT)) {
@@ -303,23 +305,21 @@
          *            choose from.
          * @param preference The preference to set the entries for.
          */
-        private void setEntriesForSelection(SelectedPictureSizes selectedSizes,
+        private void setEntriesForSelection(List<Size> selectedSizes,
                 ListPreference preference) {
             if (selectedSizes == null) {
                 return;
             }
 
-            // Avoid adding double entries at the bottom of the list which
-            // indicates that not at least 3 sizes are supported.
-            ArrayList<String> entries = new ArrayList<String>();
-            entries.add(getSizeSummaryString(selectedSizes.large));
-            if (selectedSizes.medium != selectedSizes.large) {
-                entries.add(getSizeSummaryString(selectedSizes.medium));
+            String[] entries = new String[selectedSizes.size()];
+            String[] entryValues = new String[selectedSizes.size()];
+            for (int i = 0; i < selectedSizes.size(); i++) {
+                Size size = selectedSizes.get(i);
+                entries[i] = getSizeSummaryString(size);
+                entryValues[i] = SettingsUtil.sizeToSetting(size);
             }
-            if (selectedSizes.small != selectedSizes.medium) {
-                entries.add(getSizeSummaryString(selectedSizes.small));
-            }
-            preference.setEntries(entries.toArray(new String[0]));
+            preference.setEntries(entries);
+            preference.setEntryValues(entryValues);
         }
 
         /**
@@ -351,16 +351,19 @@
         /**
          * Sets the summary for the given list preference.
          *
-         * @param selectedSizes The selected picture sizes.
+         * @param oldPictureSizes The old selected picture sizes for small medium and large
+         * @param displayableSizes The human readable preferred sizes
          * @param preference The preference for which to set the summary.
          */
-        private void setSummaryForSelection(SelectedPictureSizes selectedSizes,
-                ListPreference preference) {
-            if (selectedSizes == null) {
+        private void setSummaryForSelection(SelectedPictureSizes oldPictureSizes,
+                List<Size> displayableSizes, ListPreference preference) {
+            if (oldPictureSizes == null) {
                 return;
             }
 
-            Size selectedSize = selectedSizes.getFromSetting(preference.getValue());
+            String setting = preference.getValue();
+            Size selectedSize = oldPictureSizes.getFromSetting(setting, displayableSizes);
+
             preference.setSummary(getSizeSummaryString(selectedSize));
         }
 
@@ -390,17 +393,13 @@
             // Back camera.
             int backCameraId = getCameraId(CameraInfo.CAMERA_FACING_BACK);
             if (backCameraId >= 0) {
-                // Check whether we cached the sizes:
-                mPictureSizesBack = SettingsUtil.getSelectedCameraPictureSizes(null, backCameraId);
-                if (mPictureSizesBack == null) {
-                    Camera backCamera = Camera.open(backCameraId);
-                    if (backCamera != null) {
-                        List<Size> sizes = Size.buildListFromCameraSizes(
-                                backCamera.getParameters().getSupportedPictureSizes());
-                        backCamera.release();
-                        mPictureSizesBack = SettingsUtil.getSelectedCameraPictureSizes(sizes,
-                                backCameraId);
-                    }
+                Camera backCamera = Camera.open(backCameraId);
+                if (backCamera != null) {
+                    List<Size> sizes = Size.buildListFromCameraSizes(backCamera.getParameters().getSupportedPictureSizes());
+                    backCamera.release();
+                    mOldPictureSizesBack = SettingsUtil.getSelectedCameraPictureSizes(sizes,
+                            backCameraId);
+                    mPictureSizesBack = ResolutionUtil.getDisplayableSizesFromSupported(sizes);
                 }
                 mVideoQualitiesBack = SettingsUtil.getSelectedVideoQualities(backCameraId);
             } else {
@@ -411,17 +410,14 @@
             // Front camera.
             int frontCameraId = getCameraId(CameraInfo.CAMERA_FACING_FRONT);
             if (frontCameraId >= 0) {
-                mPictureSizesFront = SettingsUtil.getSelectedCameraPictureSizes(null,
-                        frontCameraId);
-                if (mPictureSizesFront == null) {
-                    Camera frontCamera = Camera.open(frontCameraId);
-                    if (frontCamera != null) {
-                        List<Size> sizes = Size.buildListFromCameraSizes(
-                                frontCamera.getParameters().getSupportedPictureSizes());
-                        frontCamera.release();
-                        mPictureSizesFront = SettingsUtil.getSelectedCameraPictureSizes(sizes,
-                                frontCameraId);
-                    }
+                Camera frontCamera = Camera.open(frontCameraId);
+                if (frontCamera != null) {
+                    List<Size> sizes = Size.buildListFromCameraSizes(frontCamera.getParameters().getSupportedPictureSizes());
+                    frontCamera.release();
+                    mOldPictureSizesFront= SettingsUtil.getSelectedCameraPictureSizes(sizes,
+                            frontCameraId);
+                    mPictureSizesFront =
+                            ResolutionUtil.getDisplayableSizesFromSupported(sizes);
                 }
                 mVideoQualitiesFront = SettingsUtil.getSelectedVideoQualities(frontCameraId);
             } else {
@@ -456,8 +452,9 @@
          *         picture size in megapixels.
          */
         private String getSizeSummaryString(Size size) {
+            String aspectRatio = ResolutionUtil.aspectRatioDescription(size);
             String megaPixels = sMegaPixelFormat.format((size.width() * size.height()) / 1e6);
-            return getResources().getString(R.string.setting_summary_x_megapixels, megaPixels);
+            return "(" + aspectRatio + ") " + getResources().getString(R.string.setting_summary_x_megapixels, megaPixels);
         }
     }
 }
diff --git a/src/com/android/camera/settings/ResolutionUtil.java b/src/com/android/camera/settings/ResolutionUtil.java
new file mode 100644
index 0000000..ba914bc
--- /dev/null
+++ b/src/com/android/camera/settings/ResolutionUtil.java
@@ -0,0 +1,253 @@
+/*
+ * 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 com.android.camera.util.Size;
+
+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.LinkedList;
+import java.util.List;
+
+/**
+ * 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 {
+
+    /**
+     * These are the preferred aspect ratios for the settings. We will take HAL
+     * supported aspect ratios that are within 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 final float RATIO_TOLERANCE = .05f;
+
+    /**
+     * 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
+     * @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) {
+        List<ResolutionBucket> buckets = parseAvailableSizes(sizes);
+
+        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) <= 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 RATIO_TOLERANCE, or the
+     *         original ratio
+     */
+    private static float fuzzAspectRatio(float aspectRatio) {
+        for (float desiredAspectRatio : sDesiredAspectRatios) {
+            if ((Math.abs(aspectRatio - desiredAspectRatio)) < 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
+     * @return all of the sizes grouped by their closest aspect ratio
+     */
+    private static List<ResolutionBucket> parseAvailableSizes(List<Size> sizes) {
+        HashMap<Float, ResolutionBucket> aspectRatioToBuckets = new HashMap<Float, ResolutionBucket>();
+
+        for (Size size : sizes) {
+            Float aspectRatio = size.width() / (float) size.height();
+            // 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);
+        }
+        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) {
+        BigInteger width = BigInteger.valueOf(size.width());
+        BigInteger height = BigInteger.valueOf(size.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 numerator + "x" + denominator;
+    }
+}
diff --git a/src/com/android/camera/settings/SettingsManager.java b/src/com/android/camera/settings/SettingsManager.java
index 3e2e6c3..3a48af2 100644
--- a/src/com/android/camera/settings/SettingsManager.java
+++ b/src/com/android/camera/settings/SettingsManager.java
@@ -960,16 +960,14 @@
     }
     public static Setting getPictureSizeBackSetting(Context context) {
         String defaultValue = null;
-        String[] values = context.getResources().getStringArray(
-                R.array.pref_camera_picturesize_entryvalues);
+        String[] values = null;
         return new Setting(SOURCE_DEFAULT, TYPE_STRING, defaultValue, KEY_PICTURE_SIZE_BACK,
                 values, FLUSH_OFF);
     }
 
     public static Setting getPictureSizeFrontSetting(Context context) {
         String defaultValue = null;
-        String[] values = context.getResources().getStringArray(
-                R.array.pref_camera_picturesize_entryvalues);
+        String[] values = null;
         return new Setting(SOURCE_DEFAULT, TYPE_STRING, defaultValue, KEY_PICTURE_SIZE_FRONT,
                 values, FLUSH_OFF);
     }
diff --git a/src/com/android/camera/settings/SettingsUtil.java b/src/com/android/camera/settings/SettingsUtil.java
index b14f5e2..db37c9a 100644
--- a/src/com/android/camera/settings/SettingsUtil.java
+++ b/src/com/android/camera/settings/SettingsUtil.java
@@ -45,20 +45,35 @@
         public Size medium;
         public Size small;
 
-        public Size getFromSetting(String sizeSetting) {
-            // Sanitize the value to be either small, medium or large. Default
-            // to the latter.
-            if (!SIZE_SMALL.equals(sizeSetting) && !SIZE_MEDIUM.equals(sizeSetting)) {
-                sizeSetting = SIZE_LARGE;
-            }
-
+        /**
+         * This takes a string preference describing the desired resolution and
+         * returns the camera size it represents. <br/>
+         * It supports historical values of SIZE_LARGE, SIZE_MEDIUM, and
+         * SIZE_SMALL as well as resolutions separated by an x i.e. "1024x576" <br/>
+         * If it fails to parse the string, it will return the old SIZE_LARGE
+         * value.
+         * 
+         * @param sizeSetting the preference string to convert to a size
+         * @param supportedSizes all possible camera sizes that are supported
+         * @return the size that this setting represents
+         */
+        public Size getFromSetting(String sizeSetting, List<Size> supportedSizes) {
             if (SIZE_LARGE.equals(sizeSetting)) {
                 return large;
             } else if (SIZE_MEDIUM.equals(sizeSetting)) {
                 return medium;
-            } else {
+            } else if (SIZE_SMALL.equals(sizeSetting)) {
                 return small;
+            } else if (sizeSetting != null && sizeSetting.split("x").length == 2) {
+                String[] parts = sizeSetting.split("x");
+                for (Size size : supportedSizes) {
+                    if (size.width() == Integer.valueOf(parts[0]) &&
+                            size.height() == Integer.valueOf(parts[1])) {
+                        return size;
+                    }
+                }
             }
+            return large;
         }
 
         @Override
@@ -159,7 +174,8 @@
      */
     private static Size getCameraPictureSize(String sizeSetting, List<Size> supported,
             int cameraId) {
-        return getSelectedCameraPictureSizes(supported, cameraId).getFromSetting(sizeSetting);
+        return getSelectedCameraPictureSizes(supported, cameraId).getFromSetting(sizeSetting,
+                supported);
     }
 
     /**
@@ -349,6 +365,16 @@
     }
 
     /**
+     * This is used to serialize a size to a string for storage in settings
+     * 
+     * @param size The size to serialize.
+     * @return the string to be saved in preferences
+     */
+    public static String sizeToSetting(Size size) {
+        return ((Integer) size.width()).toString() + "x" + ((Integer) size.height()).toString();
+    }
+
+    /**
      * Determines and returns the capabilities of the given camera.
      */
     public static SettingsCapabilities