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