Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2014 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.AlertDialog;
     20 import android.content.Context;
     21 import android.content.DialogInterface;
     22 import android.content.res.Resources;
     23 import android.media.CamcorderProfile;
     24 import android.util.SparseArray;
     25 
     26 import com.android.camera.debug.Log;
     27 import com.android.camera.util.ApiHelper;
     28 import com.android.camera.util.Callback;
     29 import com.android.camera.util.Size;
     30 import com.android.camera2.R;
     31 import com.android.ex.camera2.portability.CameraDeviceInfo;
     32 import com.android.ex.camera2.portability.CameraSettings;
     33 
     34 import java.util.ArrayList;
     35 import java.util.Collections;
     36 import java.util.Comparator;
     37 import java.util.LinkedList;
     38 import java.util.List;
     39 
     40 /**
     41  * Utility functions around camera settings.
     42  */
     43 public class SettingsUtil {
     44     /**
     45      * Returns the maximum video recording duration (in milliseconds).
     46      */
     47     public static int getMaxVideoDuration(Context context) {
     48         int duration = 0; // in milliseconds, 0 means unlimited.
     49         try {
     50             duration = context.getResources().getInteger(R.integer.max_video_recording_length);
     51         } catch (Resources.NotFoundException ex) {
     52         }
     53         return duration;
     54     }
     55 
     56     /** The selected Camera sizes. */
     57     public static class SelectedPictureSizes {
     58         public Size large;
     59         public Size medium;
     60         public Size small;
     61 
     62         /**
     63          * This takes a string preference describing the desired resolution and
     64          * returns the camera size it represents. <br/>
     65          * It supports historical values of SIZE_LARGE, SIZE_MEDIUM, and
     66          * SIZE_SMALL as well as resolutions separated by an x i.e. "1024x576" <br/>
     67          * If it fails to parse the string, it will return the old SIZE_LARGE
     68          * value.
     69          *
     70          * @param sizeSetting the preference string to convert to a size
     71          * @param supportedSizes all possible camera sizes that are supported
     72          * @return the size that this setting represents
     73          */
     74         public Size getFromSetting(String sizeSetting, List<Size> supportedSizes) {
     75             if (SIZE_LARGE.equals(sizeSetting)) {
     76                 return large;
     77             } else if (SIZE_MEDIUM.equals(sizeSetting)) {
     78                 return medium;
     79             } else if (SIZE_SMALL.equals(sizeSetting)) {
     80                 return small;
     81             } else if (sizeSetting != null && sizeSetting.split("x").length == 2) {
     82                 Size desiredSize = sizeFromSettingString(sizeSetting);
     83                 if (supportedSizes.contains(desiredSize)) {
     84                     return desiredSize;
     85                 }
     86             }
     87             return large;
     88         }
     89 
     90         @Override
     91         public String toString() {
     92             return "SelectedPictureSizes: " + large + ", " + medium + ", " + small;
     93         }
     94     }
     95 
     96     /** The selected {@link CamcorderProfile} qualities. */
     97     public static class SelectedVideoQualities {
     98         public int large = -1;
     99         public int medium = -1;
    100         public int small = -1;
    101 
    102         public int getFromSetting(String sizeSetting) {
    103             // Sanitize the value to be either small, medium or large. Default
    104             // to the latter.
    105             if (!SIZE_SMALL.equals(sizeSetting) && !SIZE_MEDIUM.equals(sizeSetting)) {
    106                 sizeSetting = SIZE_LARGE;
    107             }
    108 
    109             if (SIZE_LARGE.equals(sizeSetting)) {
    110                 return large;
    111             } else if (SIZE_MEDIUM.equals(sizeSetting)) {
    112                 return medium;
    113             } else {
    114                 return small;
    115             }
    116         }
    117     }
    118 
    119     private static final Log.Tag TAG = new Log.Tag("SettingsUtil");
    120 
    121     /** Enable debug output. */
    122     private static final boolean DEBUG = false;
    123 
    124     private static final String SIZE_LARGE = "large";
    125     private static final String SIZE_MEDIUM = "medium";
    126     private static final String SIZE_SMALL = "small";
    127 
    128     /** The ideal "medium" picture size is 50% of "large". */
    129     private static final float MEDIUM_RELATIVE_PICTURE_SIZE = 0.5f;
    130 
    131     /** The ideal "small" picture size is 25% of "large". */
    132     private static final float SMALL_RELATIVE_PICTURE_SIZE = 0.25f;
    133 
    134     /** Video qualities sorted by size. */
    135     public static int[] sVideoQualities = new int[] {
    136             CamcorderProfile.QUALITY_2160P,
    137             CamcorderProfile.QUALITY_1080P,
    138             CamcorderProfile.QUALITY_720P,
    139             CamcorderProfile.QUALITY_480P,
    140             CamcorderProfile.QUALITY_CIF,
    141             CamcorderProfile.QUALITY_QVGA,
    142             CamcorderProfile.QUALITY_QCIF
    143     };
    144 
    145     public static SparseArray<SelectedPictureSizes> sCachedSelectedPictureSizes =
    146             new SparseArray<SelectedPictureSizes>(2);
    147     public static SparseArray<SelectedVideoQualities> sCachedSelectedVideoQualities =
    148             new SparseArray<SelectedVideoQualities>(2);
    149 
    150     /**
    151      * Based on the selected size, this method returns the matching concrete
    152      * resolution.
    153      *
    154      * @param sizeSetting The setting selected by the user. One of "large",
    155      *            "medium, "small".
    156      * @param supported The list of supported resolutions.
    157      * @param cameraId This is used for caching the results for finding the
    158      *            different sizes.
    159      */
    160     public static Size getPhotoSize(String sizeSetting, List<Size> supported, int cameraId) {
    161         if (ResolutionUtil.NEXUS_5_LARGE_16_BY_9.equals(sizeSetting)) {
    162             return ResolutionUtil.NEXUS_5_LARGE_16_BY_9_SIZE;
    163         }
    164         Size selectedSize = getCameraPictureSize(sizeSetting, supported, cameraId);
    165         return selectedSize;
    166     }
    167 
    168     /**
    169      * Based on the selected size (large, medium or small), and the list of
    170      * supported resolutions, this method selects and returns the best matching
    171      * picture size.
    172      *
    173      * @param sizeSetting The setting selected by the user. One of "large",
    174      *            "medium, "small".
    175      * @param supported The list of supported resolutions.
    176      * @param cameraId This is used for caching the results for finding the
    177      *            different sizes.
    178      * @return The selected size.
    179      */
    180     private static Size getCameraPictureSize(String sizeSetting, List<Size> supported,
    181             int cameraId) {
    182         return getSelectedCameraPictureSizes(supported, cameraId).getFromSetting(sizeSetting,
    183                 supported);
    184     }
    185 
    186     /**
    187      * Based on the list of supported resolutions, this method selects the ones
    188      * that shall be selected for being 'large', 'medium' and 'small'.
    189      *
    190      * @return It's guaranteed that all three sizes are filled. If less than
    191      *         three sizes are supported, the selected sizes might contain
    192      *         duplicates.
    193      */
    194     static SelectedPictureSizes getSelectedCameraPictureSizes(List<Size> supported, int cameraId) {
    195         List<Size> supportedCopy = new LinkedList<Size>(supported);
    196         if (sCachedSelectedPictureSizes.get(cameraId) != null) {
    197             return sCachedSelectedPictureSizes.get(cameraId);
    198         }
    199         if (supportedCopy == null) {
    200             return null;
    201         }
    202 
    203         SelectedPictureSizes selectedSizes = new SelectedPictureSizes();
    204 
    205         // Sort supported sizes by total pixel count, descending.
    206         Collections.sort(supportedCopy, new Comparator<Size>() {
    207             @Override
    208             public int compare(Size lhs, Size rhs) {
    209                 int leftArea = lhs.width() * lhs.height();
    210                 int rightArea = rhs.width() * rhs.height();
    211                 return rightArea - leftArea;
    212             }
    213         });
    214         if (DEBUG) {
    215             Log.d(TAG, "Supported Sizes:");
    216             for (Size size : supportedCopy) {
    217                 Log.d(TAG, " --> " + size.width() + "x" + size.height() + "  "
    218                         + ((size.width() * size.height()) / 1000000f) + " - "
    219                         + (size.width() / (float) size.height()));
    220             }
    221         }
    222 
    223         // Large size is always the size with the most pixels reported.
    224         selectedSizes.large = supportedCopy.remove(0);
    225 
    226         // If possible we want to find medium and small sizes with the same
    227         // aspect ratio as 'large'.
    228         final float targetAspectRatio = selectedSizes.large.width()
    229                 / (float) selectedSizes.large.height();
    230 
    231         // Create a list of sizes with the same aspect ratio as "large" which we
    232         // will search in primarily.
    233         ArrayList<Size> aspectRatioMatches = new ArrayList<Size>();
    234         for (Size size : supportedCopy) {
    235             float aspectRatio = size.width() / (float) size.height();
    236             // Allow for small rounding errors in aspect ratio.
    237             if (Math.abs(aspectRatio - targetAspectRatio) < 0.01) {
    238                 aspectRatioMatches.add(size);
    239             }
    240         }
    241 
    242         // If we have at least two more resolutions that match the 'large'
    243         // aspect ratio, use that list to find small and medium sizes. If not,
    244         // use the full list with any aspect ratio.
    245         final List<Size> searchList = (aspectRatioMatches.size() >= 2) ? aspectRatioMatches
    246                 : supportedCopy;
    247 
    248         // Edge cases: If there are no further supported resolutions, use the
    249         // only one we have.
    250         // If there is only one remaining, use it for small and medium. If there
    251         // are two, use the two for small and medium.
    252         // These edge cases should never happen on a real device, but might
    253         // happen on test devices and emulators.
    254         if (searchList.isEmpty()) {
    255             Log.w(TAG, "Only one supported resolution.");
    256             selectedSizes.medium = selectedSizes.large;
    257             selectedSizes.small = selectedSizes.large;
    258         } else if (searchList.size() == 1) {
    259             Log.w(TAG, "Only two supported resolutions.");
    260             selectedSizes.medium = searchList.get(0);
    261             selectedSizes.small = searchList.get(0);
    262         } else if (searchList.size() == 2) {
    263             Log.w(TAG, "Exactly three supported resolutions.");
    264             selectedSizes.medium = searchList.get(0);
    265             selectedSizes.small = searchList.get(1);
    266         } else {
    267 
    268             // Based on the large pixel count, determine the target pixel count
    269             // for medium and small.
    270             final int largePixelCount = selectedSizes.large.width() * selectedSizes.large.height();
    271             final int mediumTargetPixelCount = (int) (largePixelCount * MEDIUM_RELATIVE_PICTURE_SIZE);
    272             final int smallTargetPixelCount = (int) (largePixelCount * SMALL_RELATIVE_PICTURE_SIZE);
    273 
    274             int mediumSizeIndex = findClosestSize(searchList, mediumTargetPixelCount);
    275             int smallSizeIndex = findClosestSize(searchList, smallTargetPixelCount);
    276 
    277             // If the selected sizes are the same, move the small size one down
    278             // or
    279             // the medium size one up.
    280             if (searchList.get(mediumSizeIndex).equals(searchList.get(smallSizeIndex))) {
    281                 if (smallSizeIndex < (searchList.size() - 1)) {
    282                     smallSizeIndex += 1;
    283                 } else {
    284                     mediumSizeIndex -= 1;
    285                 }
    286             }
    287             selectedSizes.medium = searchList.get(mediumSizeIndex);
    288             selectedSizes.small = searchList.get(smallSizeIndex);
    289         }
    290         sCachedSelectedPictureSizes.put(cameraId, selectedSizes);
    291         return selectedSizes;
    292     }
    293 
    294     /**
    295      * Determines the video quality for large/medium/small for the given camera.
    296      * Returns the one matching the given setting. Defaults to 'large' of the
    297      * qualitySetting does not match either large. medium or small.
    298      *
    299      * @param qualitySetting One of 'large', 'medium', 'small'.
    300      * @param cameraId The ID of the camera for which to get the quality
    301      *            setting.
    302      * @return The CamcorderProfile quality setting.
    303      */
    304     public static int getVideoQuality(String qualitySetting, int cameraId) {
    305         return getSelectedVideoQualities(cameraId).getFromSetting(qualitySetting);
    306     }
    307 
    308     static SelectedVideoQualities getSelectedVideoQualities(int cameraId) {
    309         if (sCachedSelectedVideoQualities.get(cameraId) != null) {
    310             return sCachedSelectedVideoQualities.get(cameraId);
    311         }
    312 
    313         // Go through the sizes in descending order, see if they are supported,
    314         // and set large/medium/small accordingly.
    315         // If no quality is supported at all, the first call to
    316         // getNextSupportedQuality will throw an exception.
    317         // If only one quality is supported, then all three selected qualities
    318         // will be the same.
    319         int largeIndex = getNextSupportedVideoQualityIndex(cameraId, -1);
    320         int mediumIndex = getNextSupportedVideoQualityIndex(cameraId, largeIndex);
    321         int smallIndex = getNextSupportedVideoQualityIndex(cameraId, mediumIndex);
    322 
    323         SelectedVideoQualities selectedQualities = new SelectedVideoQualities();
    324         selectedQualities.large = sVideoQualities[largeIndex];
    325         selectedQualities.medium = sVideoQualities[mediumIndex];
    326         selectedQualities.small = sVideoQualities[smallIndex];
    327         sCachedSelectedVideoQualities.put(cameraId, selectedQualities);
    328         return selectedQualities;
    329     }
    330 
    331     /**
    332      * Starting from 'start' this method returns the next supported video
    333      * quality.
    334      */
    335     private static int getNextSupportedVideoQualityIndex(int cameraId, int start) {
    336         for (int i = start + 1; i < sVideoQualities.length; ++i) {
    337             if (isVideoQualitySupported(sVideoQualities[i])
    338                     && CamcorderProfile.hasProfile(cameraId, sVideoQualities[i])) {
    339                 // We found a new supported quality.
    340                 return i;
    341             }
    342         }
    343 
    344         // Failed to find another supported quality.
    345         if (start < 0 || start >= sVideoQualities.length) {
    346             // This means we couldn't find any supported quality.
    347             throw new IllegalArgumentException("Could not find supported video qualities.");
    348         }
    349 
    350         // We previously found a larger supported size. In this edge case, just
    351         // return the same index as the previous size.
    352         return start;
    353     }
    354 
    355     /**
    356      * @return Whether the given {@link CamcorderProfile} is supported on the
    357      *         current device/OS version.
    358      */
    359     private static boolean isVideoQualitySupported(int videoQuality) {
    360         // 4k is only supported on L or higher but some devices falsely report
    361         // to have support for it on K, see b/18172081.
    362         if (!ApiHelper.isLOrHigher() && videoQuality == CamcorderProfile.QUALITY_2160P) {
    363             return false;
    364         }
    365         return true;
    366     }
    367 
    368     /**
    369      * Returns the index of the size within the given list that is closest to
    370      * the given target pixel count.
    371      */
    372     private static int findClosestSize(List<Size> sortedSizes, int targetPixelCount) {
    373         int closestMatchIndex = 0;
    374         int closestMatchPixelCountDiff = Integer.MAX_VALUE;
    375 
    376         for (int i = 0; i < sortedSizes.size(); ++i) {
    377             Size size = sortedSizes.get(i);
    378             int pixelCountDiff = Math.abs((size.width() * size.height()) - targetPixelCount);
    379             if (pixelCountDiff < closestMatchPixelCountDiff) {
    380                 closestMatchIndex = i;
    381                 closestMatchPixelCountDiff = pixelCountDiff;
    382             }
    383         }
    384         return closestMatchIndex;
    385     }
    386 
    387     private static final String SIZE_SETTING_STRING_DIMENSION_DELIMITER = "x";
    388 
    389     /**
    390      * This is used to serialize a size to a string for storage in settings
    391      *
    392      * @param size The size to serialize.
    393      * @return the string to be saved in preferences
    394      */
    395     public static String sizeToSettingString(Size size) {
    396         return size.width() + SIZE_SETTING_STRING_DIMENSION_DELIMITER + size.height();
    397     }
    398 
    399     /**
    400      * This parses a setting string and returns the representative size.
    401      *
    402      * @param sizeSettingString The string that stored in settings to represent a size.
    403      * @return the represented Size.
    404      */
    405     public static Size sizeFromSettingString(String sizeSettingString) {
    406         if (sizeSettingString == null) {
    407             return null;
    408         }
    409         String[] parts = sizeSettingString.split(SIZE_SETTING_STRING_DIMENSION_DELIMITER);
    410         if (parts.length != 2) {
    411             return null;
    412         }
    413 
    414         try {
    415             int width = Integer.parseInt(parts[0]);
    416             int height = Integer.parseInt(parts[1]);
    417             return new Size(width, height);
    418         } catch (NumberFormatException ex) {
    419             return null;
    420         }
    421     }
    422 
    423     /**
    424      * Updates an AlertDialog.Builder to explain what it means to enable
    425      * location on captures.
    426      */
    427     public static AlertDialog.Builder getFirstTimeLocationAlertBuilder(
    428             AlertDialog.Builder builder, Callback<Boolean> callback) {
    429         if (callback == null) {
    430             return null;
    431         }
    432 
    433         getLocationAlertBuilder(builder, callback)
    434                 .setMessage(R.string.remember_location_prompt);
    435 
    436         return builder;
    437     }
    438 
    439     /**
    440      * Updates an AlertDialog.Builder for choosing whether to include location
    441      * on captures.
    442      */
    443     public static AlertDialog.Builder getLocationAlertBuilder(AlertDialog.Builder builder,
    444             final Callback<Boolean> callback) {
    445         if (callback == null) {
    446             return null;
    447         }
    448 
    449         builder.setTitle(R.string.remember_location_title)
    450                 .setPositiveButton(R.string.remember_location_yes,
    451                         new DialogInterface.OnClickListener() {
    452                             @Override
    453                             public void onClick(DialogInterface dialog, int arg1) {
    454                                 callback.onCallback(true);
    455                             }
    456                         })
    457                 .setNegativeButton(R.string.remember_location_no,
    458                         new DialogInterface.OnClickListener() {
    459                             @Override
    460                             public void onClick(DialogInterface dialog, int arg1) {
    461                                 callback.onCallback(false);
    462                             }
    463                         });
    464 
    465         return builder;
    466     }
    467 
    468     /**
    469      * Gets the first (lowest-indexed) camera matching the given criterion.
    470      *
    471      * @param facing Either {@link CAMERA_FACING_BACK}, {@link CAMERA_FACING_FRONT}, or some other
    472      *               implementation of {@link CameraDeviceSelector}.
    473      * @return The ID of the first camera matching the supplied criterion, or
    474      *         -1, if no camera meeting the specification was found.
    475      */
    476     public static int getCameraId(CameraDeviceInfo info, CameraDeviceSelector chooser) {
    477         if (info == null) {
    478             return -1;
    479         }
    480         int numCameras = info.getNumberOfCameras();
    481         for (int i = 0; i < numCameras; ++i) {
    482             CameraDeviceInfo.Characteristics props = info.getCharacteristics(i);
    483             if (props == null) {
    484                 // Skip this device entry
    485                 continue;
    486             }
    487             if (chooser.useCamera(props)) {
    488                 return i;
    489             }
    490         }
    491         return -1;
    492     }
    493 
    494     public static interface CameraDeviceSelector {
    495         /**
    496          * Given the static characteristics of a specific camera device, decide whether it is the
    497          * one we will use.
    498          *
    499          * @param info The static characteristics of a device.
    500          * @return Whether we're electing to use this particular device.
    501          */
    502         public boolean useCamera(CameraDeviceInfo.Characteristics info);
    503     }
    504 
    505     public static final CameraDeviceSelector CAMERA_FACING_BACK = new CameraDeviceSelector() {
    506         @Override
    507         public boolean useCamera(CameraDeviceInfo.Characteristics info) {
    508             return info.isFacingBack();
    509         }};
    510 
    511     public static final CameraDeviceSelector CAMERA_FACING_FRONT = new CameraDeviceSelector() {
    512         @Override
    513         public boolean useCamera(CameraDeviceInfo.Characteristics info) {
    514             return info.isFacingFront();
    515         }};
    516 }
    517