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.camera2.R; 30 import com.android.ex.camera2.portability.CameraDeviceInfo; 31 import com.android.ex.camera2.portability.CameraSettings; 32 import com.android.ex.camera2.portability.Size; 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 = sizeFromString(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 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 */ setCameraPictureSize(String sizeSetting, List<Size> supported, CameraSettings settings, int cameraId)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.width() + "x" + 166 selectedSize.height()); 167 settings.setPhotoSize(selectedSize); 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 */ getPhotoSize(String sizeSetting, List<Size> supported, int cameraId)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 */ getCameraPictureSize(String sizeSetting, List<Size> supported, int cameraId)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 */ getSelectedCameraPictureSizes(List<Size> supported, int cameraId)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 */ getVideoQuality(String qualitySetting, int cameraId)324 public static int getVideoQuality(String qualitySetting, int cameraId) { 325 return getSelectedVideoQualities(cameraId).getFromSetting(qualitySetting); 326 } 327 getSelectedVideoQualities(int cameraId)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 */ getNextSupportedVideoQualityIndex(int cameraId, int start)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 */ isVideoQualitySupported(int videoQuality)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 */ findClosestSize(List<Size> sortedSizes, int targetPixelCount)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 /** 408 * This is used to serialize a size to a string for storage in settings 409 * 410 * @param size The size to serialize. 411 * @return the string to be saved in preferences 412 */ sizeToSetting(Size size)413 public static String sizeToSetting(Size size) { 414 return ((Integer) size.width()).toString() + "x" + ((Integer) size.height()).toString(); 415 } 416 417 /** 418 * This parses a setting string and returns the representative size. 419 * 420 * @param sizeSetting The string to parse. 421 * @return the represented Size. 422 */ sizeFromString(String sizeSetting)423 static public Size sizeFromString(String sizeSetting) { 424 String[] parts = sizeSetting.split("x"); 425 if (parts.length == 2) { 426 return new Size(Integer.valueOf(parts[0]), Integer.valueOf(parts[1])); 427 } else { 428 return null; 429 } 430 } 431 432 /** 433 * Updates an AlertDialog.Builder to explain what it means to enable 434 * location on captures. 435 */ getFirstTimeLocationAlertBuilder( AlertDialog.Builder builder, Callback<Boolean> callback)436 public static AlertDialog.Builder getFirstTimeLocationAlertBuilder( 437 AlertDialog.Builder builder, Callback<Boolean> callback) { 438 if (callback == null) { 439 return null; 440 } 441 442 getLocationAlertBuilder(builder, callback) 443 .setMessage(R.string.remember_location_prompt); 444 445 return builder; 446 } 447 448 /** 449 * Updates an AlertDialog.Builder for choosing whether to include location 450 * on captures. 451 */ getLocationAlertBuilder(AlertDialog.Builder builder, final Callback<Boolean> callback)452 public static AlertDialog.Builder getLocationAlertBuilder(AlertDialog.Builder builder, 453 final Callback<Boolean> callback) { 454 if (callback == null) { 455 return null; 456 } 457 458 builder.setTitle(R.string.remember_location_title) 459 .setPositiveButton(R.string.remember_location_yes, 460 new DialogInterface.OnClickListener() { 461 @Override 462 public void onClick(DialogInterface dialog, int arg1) { 463 callback.onCallback(true); 464 } 465 }) 466 .setNegativeButton(R.string.remember_location_no, 467 new DialogInterface.OnClickListener() { 468 @Override 469 public void onClick(DialogInterface dialog, int arg1) { 470 callback.onCallback(false); 471 } 472 }); 473 474 return builder; 475 } 476 477 /** 478 * Gets the first (lowest-indexed) camera matching the given criterion. 479 * 480 * @param facing Either {@link CAMERA_FACING_BACK}, {@link CAMERA_FACING_FRONT}, or some other 481 * implementation of {@link CameraDeviceSelector}. 482 * @return The ID of the first camera matching the supplied criterion, or 483 * -1, if no camera meeting the specification was found. 484 */ getCameraId(CameraDeviceInfo info, CameraDeviceSelector chooser)485 public static int getCameraId(CameraDeviceInfo info, CameraDeviceSelector chooser) { 486 int numCameras = info.getNumberOfCameras(); 487 for (int i = 0; i < numCameras; ++i) { 488 CameraDeviceInfo.Characteristics props = info.getCharacteristics(i); 489 if (props == null) { 490 // Skip this device entry 491 continue; 492 } 493 if (chooser.useCamera(props)) { 494 return i; 495 } 496 } 497 return -1; 498 } 499 500 public static interface CameraDeviceSelector { 501 /** 502 * Given the static characteristics of a specific camera device, decide whether it is the 503 * one we will use. 504 * 505 * @param info The static characteristics of a device. 506 * @return Whether we're electing to use this particular device. 507 */ useCamera(CameraDeviceInfo.Characteristics info)508 public boolean useCamera(CameraDeviceInfo.Characteristics info); 509 } 510 511 public static final CameraDeviceSelector CAMERA_FACING_BACK = new CameraDeviceSelector() { 512 @Override 513 public boolean useCamera(CameraDeviceInfo.Characteristics info) { 514 return info.isFacingBack(); 515 }}; 516 517 public static final CameraDeviceSelector CAMERA_FACING_FRONT = new CameraDeviceSelector() { 518 @Override 519 public boolean useCamera(CameraDeviceInfo.Characteristics info) { 520 return info.isFacingFront(); 521 }}; 522 } 523