/*
|
* 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();
|
}};
|
}
|