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