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