/* * Copyright (C) 2014 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 android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.media.CamcorderProfile; import android.util.SparseArray; import com.android.camera.debug.Log; import com.android.camera.util.ApiHelper; import com.android.camera.util.Callback; import com.android.camera.util.Size; import com.android.camera2.R; import com.android.ex.camera2.portability.CameraDeviceInfo; import com.android.ex.camera2.portability.CameraSettings; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; /** * Utility functions around camera settings. */ public class SettingsUtil { /** * Returns the maximum video recording duration (in milliseconds). */ public static int getMaxVideoDuration(Context context) { int duration = 0; // in milliseconds, 0 means unlimited. try { duration = context.getResources().getInteger(R.integer.max_video_recording_length); } catch (Resources.NotFoundException ex) { } return duration; } /** The selected Camera sizes. */ public static class SelectedPictureSizes { public Size large; public Size medium; public Size small; /** * This takes a string preference describing the desired resolution and * returns the camera size it represents.
* It supports historical values of SIZE_LARGE, SIZE_MEDIUM, and * SIZE_SMALL as well as resolutions separated by an x i.e. "1024x576"
* 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 supportedSizes) { if (SIZE_LARGE.equals(sizeSetting)) { return large; } else if (SIZE_MEDIUM.equals(sizeSetting)) { return medium; } else if (SIZE_SMALL.equals(sizeSetting)) { return small; } else if (sizeSetting != null && sizeSetting.split("x").length == 2) { Size desiredSize = sizeFromSettingString(sizeSetting); if (supportedSizes.contains(desiredSize)) { return desiredSize; } } return large; } @Override public String toString() { return "SelectedPictureSizes: " + large + ", " + medium + ", " + small; } } /** The selected {@link CamcorderProfile} qualities. */ public static class SelectedVideoQualities { public int large = -1; public int medium = -1; public int small = -1; public int 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; } if (SIZE_LARGE.equals(sizeSetting)) { return large; } else if (SIZE_MEDIUM.equals(sizeSetting)) { return medium; } else { return small; } } } private static final Log.Tag TAG = new Log.Tag("SettingsUtil"); /** Enable debug output. */ private static final boolean DEBUG = false; private static final String SIZE_LARGE = "large"; private static final String SIZE_MEDIUM = "medium"; private static final String SIZE_SMALL = "small"; /** The ideal "medium" picture size is 50% of "large". */ private static final float MEDIUM_RELATIVE_PICTURE_SIZE = 0.5f; /** The ideal "small" picture size is 25% of "large". */ private static final float SMALL_RELATIVE_PICTURE_SIZE = 0.25f; /** Video qualities sorted by size. */ public static int[] sVideoQualities = new int[] { CamcorderProfile.QUALITY_2160P, CamcorderProfile.QUALITY_1080P, CamcorderProfile.QUALITY_720P, CamcorderProfile.QUALITY_480P, CamcorderProfile.QUALITY_CIF, CamcorderProfile.QUALITY_QVGA, CamcorderProfile.QUALITY_QCIF }; public static SparseArray sCachedSelectedPictureSizes = new SparseArray(2); public static SparseArray sCachedSelectedVideoQualities = new SparseArray(2); /** * Based on the selected size, this method returns the matching concrete * resolution. * * @param sizeSetting The setting selected by the user. One of "large", * "medium, "small". * @param supported The list of supported resolutions. * @param cameraId This is used for caching the results for finding the * different sizes. */ public static Size getPhotoSize(String sizeSetting, List supported, int cameraId) { if (ResolutionUtil.NEXUS_5_LARGE_16_BY_9.equals(sizeSetting)) { return ResolutionUtil.NEXUS_5_LARGE_16_BY_9_SIZE; } Size selectedSize = getCameraPictureSize(sizeSetting, supported, cameraId); return selectedSize; } /** * Based on the selected size (large, medium or small), and the list of * supported resolutions, this method selects and returns the best matching * picture size. * * @param sizeSetting The setting selected by the user. One of "large", * "medium, "small". * @param supported The list of supported resolutions. * @param cameraId This is used for caching the results for finding the * different sizes. * @return The selected size. */ private static Size getCameraPictureSize(String sizeSetting, List supported, int cameraId) { return getSelectedCameraPictureSizes(supported, cameraId).getFromSetting(sizeSetting, supported); } /** * Based on the list of supported resolutions, this method selects the ones * that shall be selected for being 'large', 'medium' and 'small'. * * @return It's guaranteed that all three sizes are filled. If less than * three sizes are supported, the selected sizes might contain * duplicates. */ static SelectedPictureSizes getSelectedCameraPictureSizes(List supported, int cameraId) { List supportedCopy = new LinkedList(supported); if (sCachedSelectedPictureSizes.get(cameraId) != null) { return sCachedSelectedPictureSizes.get(cameraId); } if (supportedCopy == null) { return null; } SelectedPictureSizes selectedSizes = new SelectedPictureSizes(); // Sort supported sizes by total pixel count, descending. Collections.sort(supportedCopy, new Comparator() { @Override public int compare(Size lhs, Size rhs) { int leftArea = lhs.width() * lhs.height(); int rightArea = rhs.width() * rhs.height(); return rightArea - leftArea; } }); if (DEBUG) { Log.d(TAG, "Supported Sizes:"); for (Size size : supportedCopy) { Log.d(TAG, " --> " + size.width() + "x" + size.height() + " " + ((size.width() * size.height()) / 1000000f) + " - " + (size.width() / (float) size.height())); } } // Large size is always the size with the most pixels reported. selectedSizes.large = supportedCopy.remove(0); // If possible we want to find medium and small sizes with the same // aspect ratio as 'large'. final float targetAspectRatio = selectedSizes.large.width() / (float) selectedSizes.large.height(); // Create a list of sizes with the same aspect ratio as "large" which we // will search in primarily. ArrayList aspectRatioMatches = new ArrayList(); for (Size size : supportedCopy) { float aspectRatio = size.width() / (float) size.height(); // Allow for small rounding errors in aspect ratio. if (Math.abs(aspectRatio - targetAspectRatio) < 0.01) { aspectRatioMatches.add(size); } } // If we have at least two more resolutions that match the 'large' // aspect ratio, use that list to find small and medium sizes. If not, // use the full list with any aspect ratio. final List searchList = (aspectRatioMatches.size() >= 2) ? aspectRatioMatches : supportedCopy; // Edge cases: If there are no further supported resolutions, use the // only one we have. // If there is only one remaining, use it for small and medium. If there // are two, use the two for small and medium. // These edge cases should never happen on a real device, but might // happen on test devices and emulators. if (searchList.isEmpty()) { Log.w(TAG, "Only one supported resolution."); selectedSizes.medium = selectedSizes.large; selectedSizes.small = selectedSizes.large; } else if (searchList.size() == 1) { Log.w(TAG, "Only two supported resolutions."); selectedSizes.medium = searchList.get(0); selectedSizes.small = searchList.get(0); } else if (searchList.size() == 2) { Log.w(TAG, "Exactly three supported resolutions."); selectedSizes.medium = searchList.get(0); selectedSizes.small = searchList.get(1); } else { // Based on the large pixel count, determine the target pixel count // for medium and small. final int largePixelCount = selectedSizes.large.width() * selectedSizes.large.height(); final int mediumTargetPixelCount = (int) (largePixelCount * MEDIUM_RELATIVE_PICTURE_SIZE); final int smallTargetPixelCount = (int) (largePixelCount * SMALL_RELATIVE_PICTURE_SIZE); int mediumSizeIndex = findClosestSize(searchList, mediumTargetPixelCount); int smallSizeIndex = findClosestSize(searchList, smallTargetPixelCount); // If the selected sizes are the same, move the small size one down // or // the medium size one up. if (searchList.get(mediumSizeIndex).equals(searchList.get(smallSizeIndex))) { if (smallSizeIndex < (searchList.size() - 1)) { smallSizeIndex += 1; } else { mediumSizeIndex -= 1; } } selectedSizes.medium = searchList.get(mediumSizeIndex); selectedSizes.small = searchList.get(smallSizeIndex); } sCachedSelectedPictureSizes.put(cameraId, selectedSizes); return selectedSizes; } /** * Determines the video quality for large/medium/small for the given camera. * Returns the one matching the given setting. Defaults to 'large' of the * qualitySetting does not match either large. medium or small. * * @param qualitySetting One of 'large', 'medium', 'small'. * @param cameraId The ID of the camera for which to get the quality * setting. * @return The CamcorderProfile quality setting. */ public static int getVideoQuality(String qualitySetting, int cameraId) { return getSelectedVideoQualities(cameraId).getFromSetting(qualitySetting); } static SelectedVideoQualities getSelectedVideoQualities(int cameraId) { if (sCachedSelectedVideoQualities.get(cameraId) != null) { return sCachedSelectedVideoQualities.get(cameraId); } // Go through the sizes in descending order, see if they are supported, // and set large/medium/small accordingly. // If no quality is supported at all, the first call to // getNextSupportedQuality will throw an exception. // If only one quality is supported, then all three selected qualities // will be the same. int largeIndex = getNextSupportedVideoQualityIndex(cameraId, -1); int mediumIndex = getNextSupportedVideoQualityIndex(cameraId, largeIndex); int smallIndex = getNextSupportedVideoQualityIndex(cameraId, mediumIndex); SelectedVideoQualities selectedQualities = new SelectedVideoQualities(); selectedQualities.large = sVideoQualities[largeIndex]; selectedQualities.medium = sVideoQualities[mediumIndex]; selectedQualities.small = sVideoQualities[smallIndex]; sCachedSelectedVideoQualities.put(cameraId, selectedQualities); return selectedQualities; } /** * Starting from 'start' this method returns the next supported video * quality. */ private static int getNextSupportedVideoQualityIndex(int cameraId, int start) { for (int i = start + 1; i < sVideoQualities.length; ++i) { if (isVideoQualitySupported(sVideoQualities[i]) && CamcorderProfile.hasProfile(cameraId, sVideoQualities[i])) { // We found a new supported quality. return i; } } // Failed to find another supported quality. if (start < 0 || start >= sVideoQualities.length) { // This means we couldn't find any supported quality. throw new IllegalArgumentException("Could not find supported video qualities."); } // We previously found a larger supported size. In this edge case, just // return the same index as the previous size. return start; } /** * @return Whether the given {@link CamcorderProfile} is supported on the * current device/OS version. */ private static boolean isVideoQualitySupported(int videoQuality) { // 4k is only supported on L or higher but some devices falsely report // to have support for it on K, see b/18172081. if (!ApiHelper.isLOrHigher() && videoQuality == CamcorderProfile.QUALITY_2160P) { return false; } return true; } /** * Returns the index of the size within the given list that is closest to * the given target pixel count. */ private static int findClosestSize(List sortedSizes, int targetPixelCount) { int closestMatchIndex = 0; int closestMatchPixelCountDiff = Integer.MAX_VALUE; for (int i = 0; i < sortedSizes.size(); ++i) { Size size = sortedSizes.get(i); int pixelCountDiff = Math.abs((size.width() * size.height()) - targetPixelCount); if (pixelCountDiff < closestMatchPixelCountDiff) { closestMatchIndex = i; closestMatchPixelCountDiff = pixelCountDiff; } } return closestMatchIndex; } private static final String SIZE_SETTING_STRING_DIMENSION_DELIMITER = "x"; /** * 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 sizeToSettingString(Size size) { return size.width() + SIZE_SETTING_STRING_DIMENSION_DELIMITER + size.height(); } /** * This parses a setting string and returns the representative size. * * @param sizeSettingString The string that stored in settings to represent a size. * @return the represented Size. */ public static Size sizeFromSettingString(String sizeSettingString) { if (sizeSettingString == null) { return null; } String[] parts = sizeSettingString.split(SIZE_SETTING_STRING_DIMENSION_DELIMITER); if (parts.length != 2) { return null; } try { int width = Integer.parseInt(parts[0]); int height = Integer.parseInt(parts[1]); return new Size(width, height); } catch (NumberFormatException ex) { return null; } } /** * Updates an AlertDialog.Builder to explain what it means to enable * location on captures. */ public static AlertDialog.Builder getFirstTimeLocationAlertBuilder( AlertDialog.Builder builder, Callback callback) { if (callback == null) { return null; } getLocationAlertBuilder(builder, callback) .setMessage(R.string.remember_location_prompt); return builder; } /** * Updates an AlertDialog.Builder for choosing whether to include location * on captures. */ public static AlertDialog.Builder getLocationAlertBuilder(AlertDialog.Builder builder, final Callback callback) { if (callback == null) { return null; } builder.setTitle(R.string.remember_location_title) .setPositiveButton(R.string.remember_location_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int arg1) { callback.onCallback(true); } }) .setNegativeButton(R.string.remember_location_no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int arg1) { callback.onCallback(false); } }); return builder; } /** * Gets the first (lowest-indexed) camera matching the given criterion. * * @param facing Either {@link CAMERA_FACING_BACK}, {@link CAMERA_FACING_FRONT}, or some other * implementation of {@link CameraDeviceSelector}. * @return The ID of the first camera matching the supplied criterion, or * -1, if no camera meeting the specification was found. */ public static int getCameraId(CameraDeviceInfo info, CameraDeviceSelector chooser) { if (info == null) { return -1; } int numCameras = info.getNumberOfCameras(); for (int i = 0; i < numCameras; ++i) { CameraDeviceInfo.Characteristics props = info.getCharacteristics(i); if (props == null) { // Skip this device entry continue; } if (chooser.useCamera(props)) { return i; } } return -1; } public static interface CameraDeviceSelector { /** * Given the static characteristics of a specific camera device, decide whether it is the * one we will use. * * @param info The static characteristics of a device. * @return Whether we're electing to use this particular device. */ public boolean useCamera(CameraDeviceInfo.Characteristics info); } public static final CameraDeviceSelector CAMERA_FACING_BACK = new CameraDeviceSelector() { @Override public boolean useCamera(CameraDeviceInfo.Characteristics info) { return info.isFacingBack(); }}; public static final CameraDeviceSelector CAMERA_FACING_FRONT = new CameraDeviceSelector() { @Override public boolean useCamera(CameraDeviceInfo.Characteristics info) { return info.isFacingFront(); }}; }