| /* |
| * 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. <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 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<SelectedPictureSizes> sCachedSelectedPictureSizes = |
| new SparseArray<SelectedPictureSizes>(2); |
| public static SparseArray<SelectedVideoQualities> sCachedSelectedVideoQualities = |
| new SparseArray<SelectedVideoQualities>(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<Size> 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<Size> 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<Size> supported, int cameraId) { |
| List<Size> supportedCopy = new LinkedList<Size>(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<Size>() { |
| @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<Size> aspectRatioMatches = new ArrayList<Size>(); |
| 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<Size> 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<Size> 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<Boolean> 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<Boolean> 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(); |
| }}; |
| } |