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