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 */ getMaxVideoDuration(Context context)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 */ getFromSetting(String sizeSetting, List<Size> supportedSizes)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 toString()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 getFromSetting(String sizeSetting)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 */ getPhotoSize(String sizeSetting, List<Size> supported, int cameraId)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 */ getCameraPictureSize(String sizeSetting, List<Size> supported, int cameraId)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 */ getSelectedCameraPictureSizes(List<Size> supported, int cameraId)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 */ getVideoQuality(String qualitySetting, int cameraId)304 public static int getVideoQuality(String qualitySetting, int cameraId) { 305 return getSelectedVideoQualities(cameraId).getFromSetting(qualitySetting); 306 } 307 getSelectedVideoQualities(int cameraId)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 */ getNextSupportedVideoQualityIndex(int cameraId, int start)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 */ isVideoQualitySupported(int videoQuality)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 */ findClosestSize(List<Size> sortedSizes, int targetPixelCount)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 */ sizeToSettingString(Size size)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 */ sizeFromSettingString(String sizeSettingString)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 */ getFirstTimeLocationAlertBuilder( AlertDialog.Builder builder, Callback<Boolean> callback)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 */ getLocationAlertBuilder(AlertDialog.Builder builder, final Callback<Boolean> callback)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 */ getCameraId(CameraDeviceInfo info, CameraDeviceSelector chooser)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 */ useCamera(CameraDeviceInfo.Characteristics info)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