SettingsUtil.java revision 6607dae6c4d1ee5bbf572695c2042d4d9129a730
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.DialogInterface;
21import android.hardware.Camera;
22import android.hardware.Camera.Parameters;
23import android.media.CamcorderProfile;
24import android.util.SparseArray;
25
26import com.android.camera.cameradevice.CameraManager;
27import com.android.camera.cameradevice.CameraSettings;
28import com.android.camera.debug.Log;
29import com.android.camera.settings.SettingsManager.SettingsCapabilities;
30import com.android.camera.util.Callback;
31import com.android.camera.cameradevice.Size;
32import com.android.camera2.R;
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    /** The selected Camera sizes. */
45    public static class SelectedPictureSizes {
46        public Size large;
47        public Size medium;
48        public Size small;
49
50        /**
51         * This takes a string preference describing the desired resolution and
52         * returns the camera size it represents. <br/>
53         * It supports historical values of SIZE_LARGE, SIZE_MEDIUM, and
54         * SIZE_SMALL as well as resolutions separated by an x i.e. "1024x576" <br/>
55         * If it fails to parse the string, it will return the old SIZE_LARGE
56         * value.
57         *
58         * @param sizeSetting the preference string to convert to a size
59         * @param supportedSizes all possible camera sizes that are supported
60         * @return the size that this setting represents
61         */
62        public Size getFromSetting(String sizeSetting, List<Size> supportedSizes) {
63            if (SIZE_LARGE.equals(sizeSetting)) {
64                return large;
65            } else if (SIZE_MEDIUM.equals(sizeSetting)) {
66                return medium;
67            } else if (SIZE_SMALL.equals(sizeSetting)) {
68                return small;
69            } else if (sizeSetting != null && sizeSetting.split("x").length == 2) {
70                Size desiredSize = sizeFromString(sizeSetting);
71                if (supportedSizes.contains(desiredSize)) {
72                    return desiredSize;
73                }
74            }
75            return large;
76        }
77
78        @Override
79        public String toString() {
80            return "SelectedPictureSizes: " + large + ", " + medium + ", " + small;
81        }
82    }
83
84    /** The selected {@link CamcorderProfile} qualities. */
85    public static class SelectedVideoQualities {
86        public int large = -1;
87        public int medium = -1;
88        public int small = -1;
89
90        public int getFromSetting(String sizeSetting) {
91            // Sanitize the value to be either small, medium or large. Default
92            // to the latter.
93            if (!SIZE_SMALL.equals(sizeSetting) && !SIZE_MEDIUM.equals(sizeSetting)) {
94                sizeSetting = SIZE_LARGE;
95            }
96
97            if (SIZE_LARGE.equals(sizeSetting)) {
98                return large;
99            } else if (SIZE_MEDIUM.equals(sizeSetting)) {
100                return medium;
101            } else {
102                return small;
103            }
104        }
105    }
106
107    private static final Log.Tag TAG = new Log.Tag("SettingsUtil");
108
109    /** Enable debug output. */
110    private static final boolean DEBUG = false;
111
112    private static final String SIZE_LARGE = "large";
113    private static final String SIZE_MEDIUM = "medium";
114    private static final String SIZE_SMALL = "small";
115
116    /** The ideal "medium" picture size is 50% of "large". */
117    private static final float MEDIUM_RELATIVE_PICTURE_SIZE = 0.5f;
118
119    /** The ideal "small" picture size is 25% of "large". */
120    private static final float SMALL_RELATIVE_PICTURE_SIZE = 0.25f;
121
122    /** Video qualities sorted by size. */
123    public static int[] sVideoQualities = new int[] {
124            CamcorderProfile.QUALITY_1080P,
125            CamcorderProfile.QUALITY_720P,
126            CamcorderProfile.QUALITY_480P,
127            CamcorderProfile.QUALITY_CIF,
128            CamcorderProfile.QUALITY_QVGA,
129            CamcorderProfile.QUALITY_QCIF
130    };
131
132    public static SparseArray<SelectedPictureSizes> sCachedSelectedPictureSizes =
133            new SparseArray<SelectedPictureSizes>(2);
134    public static SparseArray<SelectedVideoQualities> sCachedSelectedVideoQualities =
135            new SparseArray<SelectedVideoQualities>(2);
136
137    /**
138     * Based on the selected size, this method selects the matching concrete
139     * resolution and sets it as the picture size.
140     *
141     * @param sizeSetting The setting selected by the user. One of "large",
142     *            "medium, "small" or two integers separated by "x".
143     * @param supported The list of supported resolutions.
144     * @param settings The Camera settings to set the selected picture
145     *            resolution on.
146     * @param cameraId This is used for caching the results for finding the
147     *            different sizes.
148     */
149    public static void setCameraPictureSize(String sizeSetting, List<Size> supported,
150            CameraSettings settings, int cameraId) {
151        Size selectedSize = getCameraPictureSize(sizeSetting, supported, cameraId);
152        Log.d(TAG, "Selected " + sizeSetting + " resolution: " + selectedSize.width() + "x" +
153                selectedSize.height());
154        settings.setPhotoSize(selectedSize);
155    }
156
157    /**
158     * Based on the selected size, this method returns the matching concrete
159     * resolution.
160     *
161     * @param sizeSetting The setting selected by the user. One of "large",
162     *            "medium, "small".
163     * @param supported The list of supported resolutions.
164     * @param cameraId This is used for caching the results for finding the
165     *            different sizes.
166     */
167    public static Size getPhotoSize(String sizeSetting, List<Size> supported, int cameraId) {
168        if (ResolutionUtil.NEXUS_5_LARGE_16_BY_9.equals(sizeSetting)) {
169            return ResolutionUtil.NEXUS_5_LARGE_16_BY_9_SIZE;
170        }
171        Size selectedSize = getCameraPictureSize(sizeSetting, supported, cameraId);
172        return selectedSize;
173    }
174
175    /**
176     * Based on the selected size (large, medium or small), and the list of
177     * supported resolutions, this method selects and returns the best matching
178     * picture size.
179     *
180     * @param sizeSetting The setting selected by the user. One of "large",
181     *            "medium, "small".
182     * @param supported The list of supported resolutions.
183     * @param cameraId This is used for caching the results for finding the
184     *            different sizes.
185     * @return The selected size.
186     */
187    private static Size getCameraPictureSize(String sizeSetting, List<Size> supported,
188            int cameraId) {
189        return getSelectedCameraPictureSizes(supported, cameraId).getFromSetting(sizeSetting,
190                supported);
191    }
192
193    /**
194     * Based on the list of supported resolutions, this method selects the ones
195     * that shall be selected for being 'large', 'medium' and 'small'.
196     *
197     * @return It's guaranteed that all three sizes are filled. If less than
198     *         three sizes are supported, the selected sizes might contain
199     *         duplicates.
200     */
201    static SelectedPictureSizes getSelectedCameraPictureSizes(List<Size> supported, int cameraId) {
202        List<Size> supportedCopy = new LinkedList<Size>(supported);
203        if (sCachedSelectedPictureSizes.get(cameraId) != null) {
204            return sCachedSelectedPictureSizes.get(cameraId);
205        }
206        if (supportedCopy == null) {
207            return null;
208        }
209
210        SelectedPictureSizes selectedSizes = new SelectedPictureSizes();
211
212        // Sort supported sizes by total pixel count, descending.
213        Collections.sort(supportedCopy, new Comparator<Size>() {
214            @Override
215            public int compare(Size lhs, Size rhs) {
216                int leftArea = lhs.width() * lhs.height();
217                int rightArea = rhs.width() * rhs.height();
218                return rightArea - leftArea;
219            }
220        });
221        if (DEBUG) {
222            Log.d(TAG, "Supported Sizes:");
223            for (Size size : supportedCopy) {
224                Log.d(TAG, " --> " + size.width() + "x" + size.height() + "  "
225                        + ((size.width() * size.height()) / 1000000f) + " - "
226                        + (size.width() / (float) size.height()));
227            }
228        }
229
230        // Large size is always the size with the most pixels reported.
231        selectedSizes.large = supportedCopy.remove(0);
232
233        // If possible we want to find medium and small sizes with the same
234        // aspect ratio as 'large'.
235        final float targetAspectRatio = selectedSizes.large.width()
236                / (float) selectedSizes.large.height();
237
238        // Create a list of sizes with the same aspect ratio as "large" which we
239        // will search in primarily.
240        ArrayList<Size> aspectRatioMatches = new ArrayList<Size>();
241        for (Size size : supportedCopy) {
242            float aspectRatio = size.width() / (float) size.height();
243            // Allow for small rounding errors in aspect ratio.
244            if (Math.abs(aspectRatio - targetAspectRatio) < 0.01) {
245                aspectRatioMatches.add(size);
246            }
247        }
248
249        // If we have at least two more resolutions that match the 'large'
250        // aspect ratio, use that list to find small and medium sizes. If not,
251        // use the full list with any aspect ratio.
252        final List<Size> searchList = (aspectRatioMatches.size() >= 2) ? aspectRatioMatches
253                : supportedCopy;
254
255        // Edge cases: If there are no further supported resolutions, use the
256        // only one we have.
257        // If there is only one remaining, use it for small and medium. If there
258        // are two, use the two for small and medium.
259        // These edge cases should never happen on a real device, but might
260        // happen on test devices and emulators.
261        if (searchList.isEmpty()) {
262            Log.w(TAG, "Only one supported resolution.");
263            selectedSizes.medium = selectedSizes.large;
264            selectedSizes.small = selectedSizes.large;
265        } else if (searchList.size() == 1) {
266            Log.w(TAG, "Only two supported resolutions.");
267            selectedSizes.medium = searchList.get(0);
268            selectedSizes.small = searchList.get(0);
269        } else if (searchList.size() == 2) {
270            Log.w(TAG, "Exactly three supported resolutions.");
271            selectedSizes.medium = searchList.get(0);
272            selectedSizes.small = searchList.get(1);
273        } else {
274
275            // Based on the large pixel count, determine the target pixel count
276            // for medium and small.
277            final int largePixelCount = selectedSizes.large.width() * selectedSizes.large.height();
278            final int mediumTargetPixelCount = (int) (largePixelCount * MEDIUM_RELATIVE_PICTURE_SIZE);
279            final int smallTargetPixelCount = (int) (largePixelCount * SMALL_RELATIVE_PICTURE_SIZE);
280
281            int mediumSizeIndex = findClosestSize(searchList, mediumTargetPixelCount);
282            int smallSizeIndex = findClosestSize(searchList, smallTargetPixelCount);
283
284            // If the selected sizes are the same, move the small size one down
285            // or
286            // the medium size one up.
287            if (searchList.get(mediumSizeIndex).equals(searchList.get(smallSizeIndex))) {
288                if (smallSizeIndex < (searchList.size() - 1)) {
289                    smallSizeIndex += 1;
290                } else {
291                    mediumSizeIndex -= 1;
292                }
293            }
294            selectedSizes.medium = searchList.get(mediumSizeIndex);
295            selectedSizes.small = searchList.get(smallSizeIndex);
296        }
297        sCachedSelectedPictureSizes.put(cameraId, selectedSizes);
298        return selectedSizes;
299    }
300
301    /**
302     * Determines the video quality for large/medium/small for the given camera.
303     * Returns the one matching the given setting. Defaults to 'large' of the
304     * qualitySetting does not match either large. medium or small.
305     *
306     * @param qualitySetting One of 'large', 'medium', 'small'.
307     * @param cameraId The ID of the camera for which to get the quality
308     *            setting.
309     * @return The CamcorderProfile quality setting.
310     */
311    public static int getVideoQuality(String qualitySetting, int cameraId) {
312        return getSelectedVideoQualities(cameraId).getFromSetting(qualitySetting);
313    }
314
315    static SelectedVideoQualities getSelectedVideoQualities(int cameraId) {
316        if (sCachedSelectedVideoQualities.get(cameraId) != null) {
317            return sCachedSelectedVideoQualities.get(cameraId);
318        }
319
320        // Go through the sizes in descending order, see if they are supported,
321        // and set large/medium/small accordingly.
322        // If no quality is supported at all, the first call to
323        // getNextSupportedQuality will throw an exception.
324        // If only one quality is supported, then all three selected qualities
325        // will be the same.
326        int largeIndex = getNextSupportedVideoQualityIndex(cameraId, -1);
327        int mediumIndex = getNextSupportedVideoQualityIndex(cameraId, largeIndex);
328        int smallIndex = getNextSupportedVideoQualityIndex(cameraId, mediumIndex);
329
330        SelectedVideoQualities selectedQualities = new SelectedVideoQualities();
331        selectedQualities.large = sVideoQualities[largeIndex];
332        selectedQualities.medium = sVideoQualities[mediumIndex];
333        selectedQualities.small = sVideoQualities[smallIndex];
334        sCachedSelectedVideoQualities.put(cameraId, selectedQualities);
335        return selectedQualities;
336    }
337
338    /**
339     * Starting from 'start' this method returns the next supported video
340     * quality.
341     */
342    private static int getNextSupportedVideoQualityIndex(int cameraId, int start) {
343        for (int i = start + 1; i < sVideoQualities.length; ++i) {
344            if (CamcorderProfile.hasProfile(cameraId, sVideoQualities[i])) {
345                // We found a new supported quality.
346                return i;
347            }
348        }
349
350        // Failed to find another supported quality.
351        if (start < 0 || start >= sVideoQualities.length) {
352            // This means we couldn't find any supported quality.
353            throw new IllegalArgumentException("Could not find supported video qualities.");
354        }
355
356        // We previously found a larger supported size. In this edge case, just
357        // return the same index as the previous size.
358        return start;
359    }
360
361    /**
362     * Returns the index of the size within the given list that is closest to
363     * the given target pixel count.
364     */
365    private static int findClosestSize(List<Size> sortedSizes, int targetPixelCount) {
366        int closestMatchIndex = 0;
367        int closestMatchPixelCountDiff = Integer.MAX_VALUE;
368
369        for (int i = 0; i < sortedSizes.size(); ++i) {
370            Size size = sortedSizes.get(i);
371            int pixelCountDiff = Math.abs((size.width() * size.height()) - targetPixelCount);
372            if (pixelCountDiff < closestMatchPixelCountDiff) {
373                closestMatchIndex = i;
374                closestMatchPixelCountDiff = pixelCountDiff;
375            }
376        }
377        return closestMatchIndex;
378    }
379
380    /**
381     * This is used to serialize a size to a string for storage in settings
382     *
383     * @param size The size to serialize.
384     * @return the string to be saved in preferences
385     */
386    public static String sizeToSetting(Size size) {
387        return ((Integer) size.width()).toString() + "x" + ((Integer) size.height()).toString();
388    }
389
390    /**
391     * This parses a setting string and returns the representative size.
392     *
393     * @param sizeSetting The string to parse.
394     * @return the represented Size.
395     */
396    static public Size sizeFromString(String sizeSetting) {
397        String[] parts = sizeSetting.split("x");
398        if (parts.length == 2) {
399            return new Size(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]));
400        } else {
401            return null;
402        }
403    }
404
405    /**
406     * Determines and returns the capabilities of the given camera.
407     */
408    public static SettingsCapabilities
409            getSettingsCapabilities(CameraManager.CameraProxy camera) {
410        final Parameters parameters = camera.getParameters();
411        return (new SettingsCapabilities() {
412            @Override
413            public String[] getSupportedExposureValues() {
414                int max = parameters.getMaxExposureCompensation();
415                int min = parameters.getMinExposureCompensation();
416                float step = parameters.getExposureCompensationStep();
417                int maxValue = Math.min(3, (int) Math.floor(max * step));
418                int minValue = Math.max(-3, (int) Math.ceil(min * step));
419                String[] entryValues = new String[maxValue - minValue + 1];
420                for (int i = minValue; i <= maxValue; ++i) {
421                    entryValues[i - minValue] = Integer.toString(Math.round(i / step));
422                }
423                return entryValues;
424            }
425
426            @Override
427            public String[] getSupportedCameraIds() {
428                int numberOfCameras = Camera.getNumberOfCameras();
429                String[] cameraIds = new String[numberOfCameras];
430                for (int i = 0; i < numberOfCameras; i++) {
431                    cameraIds[i] = "" + i;
432                }
433                return cameraIds;
434            }
435        });
436    }
437
438    /**
439     * Updates an AlertDialog.Builder to explain what it means to enable
440     * location on captures.
441     */
442    public static AlertDialog.Builder getFirstTimeLocationAlertBuilder(
443            AlertDialog.Builder builder, Callback<Boolean> callback) {
444        if (callback == null) {
445            return null;
446        }
447
448        getLocationAlertBuilder(builder, callback)
449                .setMessage(R.string.remember_location_prompt);
450
451        return builder;
452    }
453
454    /**
455     * Updates an AlertDialog.Builder for choosing whether to include location
456     * on captures.
457     */
458    public static AlertDialog.Builder getLocationAlertBuilder(AlertDialog.Builder builder,
459            final Callback<Boolean> callback) {
460        if (callback == null) {
461            return null;
462        }
463
464        builder.setTitle(R.string.remember_location_title)
465                .setPositiveButton(R.string.remember_location_yes,
466                        new DialogInterface.OnClickListener() {
467                            @Override
468                            public void onClick(DialogInterface dialog, int arg1) {
469                                callback.onCallback(true);
470                            }
471                        })
472                .setNegativeButton(R.string.remember_location_no,
473                        new DialogInterface.OnClickListener() {
474                            @Override
475                            public void onClick(DialogInterface dialog, int arg1) {
476                                callback.onCallback(false);
477                            }
478                        });
479
480        return builder;
481    }
482
483    /**
484     * Gets the first camera facing the given direction.
485     *
486     * @param facing Either {@link android.hardware.Camera.CameraInfo#CAMERA_FACING_BACK} or
487     *            {@link android.hardware.Camera.CameraInfo#CAMERA_FACING_FRONT}.
488     * @return The ID of the first camera matching the given direction, or
489     *         -1, if no camera with the given facing was found.
490     */
491    public static int getCameraId(int facing) {
492        int numCameras = Camera.getNumberOfCameras();
493        for (int i = 0; i < numCameras; ++i) {
494            Camera.CameraInfo info = new Camera.CameraInfo();
495            Camera.getCameraInfo(i, info);
496            if (info.facing == facing) {
497                return i;
498            }
499        }
500        return -1;
501    }
502}
503