1 /*
2  * Copyright (C) 2014 ZXing authors
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.google.zxing.client.android.camera;
18 
19 import android.graphics.Point;
20 import android.graphics.Rect;
21 import android.hardware.Camera;
22 import android.os.Build;
23 import android.util.Log;
24 
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.regex.Pattern;
31 
32 /**
33  * Utility methods for configuring the Android camera.
34  *
35  * @author Sean Owen
36  */
37 @SuppressWarnings("deprecation") // camera APIs
38 public final class CameraConfigurationUtils {
39 
40   private static final String TAG = "CameraConfiguration";
41 
42   private static final Pattern SEMICOLON = Pattern.compile(";");
43 
44   private static final int MIN_PREVIEW_PIXELS = 480 * 320; // normal screen
45   private static final float MAX_EXPOSURE_COMPENSATION = 1.5f;
46   private static final float MIN_EXPOSURE_COMPENSATION = 0.0f;
47   private static final double MAX_ASPECT_DISTORTION = 0.15;
48   private static final int MIN_FPS = 10;
49   private static final int MAX_FPS = 20;
50   private static final int AREA_PER_1000 = 400;
51 
CameraConfigurationUtils()52   private CameraConfigurationUtils() {
53   }
54 
setFocus(Camera.Parameters parameters, boolean autoFocus, boolean disableContinuous, boolean safeMode)55   public static void setFocus(Camera.Parameters parameters,
56                               boolean autoFocus,
57                               boolean disableContinuous,
58                               boolean safeMode) {
59     List<String> supportedFocusModes = parameters.getSupportedFocusModes();
60     String focusMode = null;
61     if (autoFocus) {
62       if (safeMode || disableContinuous) {
63         focusMode = findSettableValue("focus mode",
64                                        supportedFocusModes,
65                                        Camera.Parameters.FOCUS_MODE_AUTO);
66       } else {
67         focusMode = findSettableValue("focus mode",
68                                       supportedFocusModes,
69                                       Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
70                                       Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
71                                       Camera.Parameters.FOCUS_MODE_AUTO);
72       }
73     }
74     // Maybe selected auto-focus but not available, so fall through here:
75     if (!safeMode && focusMode == null) {
76       focusMode = findSettableValue("focus mode",
77                                     supportedFocusModes,
78                                     Camera.Parameters.FOCUS_MODE_MACRO,
79                                     Camera.Parameters.FOCUS_MODE_EDOF);
80     }
81     if (focusMode != null) {
82       if (focusMode.equals(parameters.getFocusMode())) {
83         Log.i(TAG, "Focus mode already set to " + focusMode);
84       } else {
85         parameters.setFocusMode(focusMode);
86       }
87     }
88   }
89 
setTorch(Camera.Parameters parameters, boolean on)90   public static void setTorch(Camera.Parameters parameters, boolean on) {
91     List<String> supportedFlashModes = parameters.getSupportedFlashModes();
92     String flashMode;
93     if (on) {
94       flashMode = findSettableValue("flash mode",
95                                     supportedFlashModes,
96                                     Camera.Parameters.FLASH_MODE_TORCH,
97                                     Camera.Parameters.FLASH_MODE_ON);
98     } else {
99       flashMode = findSettableValue("flash mode",
100                                     supportedFlashModes,
101                                     Camera.Parameters.FLASH_MODE_OFF);
102     }
103     if (flashMode != null) {
104       if (flashMode.equals(parameters.getFlashMode())) {
105         Log.i(TAG, "Flash mode already set to " + flashMode);
106       } else {
107         Log.i(TAG, "Setting flash mode to " + flashMode);
108         parameters.setFlashMode(flashMode);
109       }
110     }
111   }
112 
setBestExposure(Camera.Parameters parameters, boolean lightOn)113   public static void setBestExposure(Camera.Parameters parameters, boolean lightOn) {
114     int minExposure = parameters.getMinExposureCompensation();
115     int maxExposure = parameters.getMaxExposureCompensation();
116     float step = parameters.getExposureCompensationStep();
117     if ((minExposure != 0 || maxExposure != 0) && step > 0.0f) {
118       // Set low when light is on
119       float targetCompensation = lightOn ? MIN_EXPOSURE_COMPENSATION : MAX_EXPOSURE_COMPENSATION;
120       int compensationSteps = Math.round(targetCompensation / step);
121       float actualCompensation = step * compensationSteps;
122       // Clamp value:
123       compensationSteps = Math.max(Math.min(compensationSteps, maxExposure), minExposure);
124       if (parameters.getExposureCompensation() == compensationSteps) {
125         Log.i(TAG, "Exposure compensation already set to " + compensationSteps + " / " + actualCompensation);
126       } else {
127         Log.i(TAG, "Setting exposure compensation to " + compensationSteps + " / " + actualCompensation);
128         parameters.setExposureCompensation(compensationSteps);
129       }
130     } else {
131       Log.i(TAG, "Camera does not support exposure compensation");
132     }
133   }
134 
setBestPreviewFPS(Camera.Parameters parameters)135   public static void setBestPreviewFPS(Camera.Parameters parameters) {
136     setBestPreviewFPS(parameters, MIN_FPS, MAX_FPS);
137   }
138 
setBestPreviewFPS(Camera.Parameters parameters, int minFPS, int maxFPS)139   public static void setBestPreviewFPS(Camera.Parameters parameters, int minFPS, int maxFPS) {
140     List<int[]> supportedPreviewFpsRanges = parameters.getSupportedPreviewFpsRange();
141     Log.i(TAG, "Supported FPS ranges: " + toString(supportedPreviewFpsRanges));
142     if (supportedPreviewFpsRanges != null && !supportedPreviewFpsRanges.isEmpty()) {
143       int[] suitableFPSRange = null;
144       for (int[] fpsRange : supportedPreviewFpsRanges) {
145         int thisMin = fpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
146         int thisMax = fpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
147         if (thisMin >= minFPS * 1000 && thisMax <= maxFPS * 1000) {
148           suitableFPSRange = fpsRange;
149           break;
150         }
151       }
152       if (suitableFPSRange == null) {
153         Log.i(TAG, "No suitable FPS range?");
154       } else {
155         int[] currentFpsRange = new int[2];
156         parameters.getPreviewFpsRange(currentFpsRange);
157         if (Arrays.equals(currentFpsRange, suitableFPSRange)) {
158           Log.i(TAG, "FPS range already set to " + Arrays.toString(suitableFPSRange));
159         } else {
160           Log.i(TAG, "Setting FPS range to " + Arrays.toString(suitableFPSRange));
161           parameters.setPreviewFpsRange(suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
162                                         suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
163         }
164       }
165     }
166   }
167 
setFocusArea(Camera.Parameters parameters)168   public static void setFocusArea(Camera.Parameters parameters) {
169     if (parameters.getMaxNumFocusAreas() > 0) {
170       Log.i(TAG, "Old focus areas: " + toString(parameters.getFocusAreas()));
171       List<Camera.Area> middleArea = buildMiddleArea();
172       Log.i(TAG, "Setting focus area to : " + toString(middleArea));
173       parameters.setFocusAreas(middleArea);
174     } else {
175       Log.i(TAG, "Device does not support focus areas");
176     }
177   }
178 
setMetering(Camera.Parameters parameters)179   public static void setMetering(Camera.Parameters parameters) {
180     if (parameters.getMaxNumMeteringAreas() > 0) {
181       Log.i(TAG, "Old metering areas: " + parameters.getMeteringAreas());
182       List<Camera.Area> middleArea = buildMiddleArea();
183       Log.i(TAG, "Setting metering area to : " + toString(middleArea));
184       parameters.setMeteringAreas(middleArea);
185     } else {
186       Log.i(TAG, "Device does not support metering areas");
187     }
188   }
189 
buildMiddleArea()190   private static List<Camera.Area> buildMiddleArea() {
191     return Collections.singletonList(
192         new Camera.Area(new Rect(-AREA_PER_1000, -AREA_PER_1000, AREA_PER_1000, AREA_PER_1000), 1));
193   }
194 
setVideoStabilization(Camera.Parameters parameters)195   public static void setVideoStabilization(Camera.Parameters parameters) {
196     if (parameters.isVideoStabilizationSupported()) {
197       if (parameters.getVideoStabilization()) {
198         Log.i(TAG, "Video stabilization already enabled");
199       } else {
200         Log.i(TAG, "Enabling video stabilization...");
201         parameters.setVideoStabilization(true);
202       }
203     } else {
204       Log.i(TAG, "This device does not support video stabilization");
205     }
206   }
207 
setBarcodeSceneMode(Camera.Parameters parameters)208   public static void setBarcodeSceneMode(Camera.Parameters parameters) {
209     if (Camera.Parameters.SCENE_MODE_BARCODE.equals(parameters.getSceneMode())) {
210       Log.i(TAG, "Barcode scene mode already set");
211       return;
212     }
213     String sceneMode = findSettableValue("scene mode",
214                                          parameters.getSupportedSceneModes(),
215                                          Camera.Parameters.SCENE_MODE_BARCODE);
216     if (sceneMode != null) {
217       parameters.setSceneMode(sceneMode);
218     }
219   }
220 
setZoom(Camera.Parameters parameters, double targetZoomRatio)221   public static void setZoom(Camera.Parameters parameters, double targetZoomRatio) {
222     if (parameters.isZoomSupported()) {
223       Integer zoom = indexOfClosestZoom(parameters, targetZoomRatio);
224       if (zoom == null) {
225         return;
226       }
227       if (parameters.getZoom() == zoom) {
228         Log.i(TAG, "Zoom is already set to " + zoom);
229       } else {
230         Log.i(TAG, "Setting zoom to " + zoom);
231         parameters.setZoom(zoom);
232       }
233     } else {
234       Log.i(TAG, "Zoom is not supported");
235     }
236   }
237 
indexOfClosestZoom(Camera.Parameters parameters, double targetZoomRatio)238   private static Integer indexOfClosestZoom(Camera.Parameters parameters, double targetZoomRatio) {
239     List<Integer> ratios = parameters.getZoomRatios();
240     Log.i(TAG, "Zoom ratios: " + ratios);
241     int maxZoom = parameters.getMaxZoom();
242     if (ratios == null || ratios.isEmpty() || ratios.size() != maxZoom + 1) {
243       Log.w(TAG, "Invalid zoom ratios!");
244       return null;
245     }
246     double target100 = 100.0 * targetZoomRatio;
247     double smallestDiff = Double.POSITIVE_INFINITY;
248     int closestIndex = 0;
249     for (int i = 0; i < ratios.size(); i++) {
250       double diff = Math.abs(ratios.get(i) - target100);
251       if (diff < smallestDiff) {
252         smallestDiff = diff;
253         closestIndex = i;
254       }
255     }
256     Log.i(TAG, "Chose zoom ratio of " + (ratios.get(closestIndex) / 100.0));
257     return closestIndex;
258   }
259 
setInvertColor(Camera.Parameters parameters)260   public static void setInvertColor(Camera.Parameters parameters) {
261     if (Camera.Parameters.EFFECT_NEGATIVE.equals(parameters.getColorEffect())) {
262       Log.i(TAG, "Negative effect already set");
263       return;
264     }
265     String colorMode = findSettableValue("color effect",
266                                          parameters.getSupportedColorEffects(),
267                                          Camera.Parameters.EFFECT_NEGATIVE);
268     if (colorMode != null) {
269       parameters.setColorEffect(colorMode);
270     }
271   }
272 
findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution)273   public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
274 
275     List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
276     if (rawSupportedSizes == null) {
277       Log.w(TAG, "Device returned no supported preview sizes; using default");
278       Camera.Size defaultSize = parameters.getPreviewSize();
279       if (defaultSize == null) {
280         throw new IllegalStateException("Parameters contained no preview size!");
281       }
282       return new Point(defaultSize.width, defaultSize.height);
283     }
284 
285     if (Log.isLoggable(TAG, Log.INFO)) {
286       StringBuilder previewSizesString = new StringBuilder();
287       for (Camera.Size size : rawSupportedSizes) {
288         previewSizesString.append(size.width).append('x').append(size.height).append(' ');
289       }
290       Log.i(TAG, "Supported preview sizes: " + previewSizesString);
291     }
292 
293     double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
294 
295     // Find a suitable size, with max resolution
296     int maxResolution = 0;
297     Camera.Size maxResPreviewSize = null;
298     for (Camera.Size size : rawSupportedSizes) {
299       int realWidth = size.width;
300       int realHeight = size.height;
301       int resolution = realWidth * realHeight;
302       if (resolution < MIN_PREVIEW_PIXELS) {
303         continue;
304       }
305 
306       boolean isCandidatePortrait = realWidth < realHeight;
307       int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
308       int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
309       double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;
310       double distortion = Math.abs(aspectRatio - screenAspectRatio);
311       if (distortion > MAX_ASPECT_DISTORTION) {
312         continue;
313       }
314 
315       if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
316         Point exactPoint = new Point(realWidth, realHeight);
317         Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
318         return exactPoint;
319       }
320 
321       // Resolution is suitable; record the one with max resolution
322       if (resolution > maxResolution) {
323         maxResolution = resolution;
324         maxResPreviewSize = size;
325       }
326     }
327 
328     // If no exact match, use largest preview size. This was not a great idea on older devices because
329     // of the additional computation needed. We're likely to get here on newer Android 4+ devices, where
330     // the CPU is much more powerful.
331     if (maxResPreviewSize != null) {
332       Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
333       Log.i(TAG, "Using largest suitable preview size: " + largestSize);
334       return largestSize;
335     }
336 
337     // If there is nothing at all suitable, return current preview size
338     Camera.Size defaultPreview = parameters.getPreviewSize();
339     if (defaultPreview == null) {
340       throw new IllegalStateException("Parameters contained no preview size!");
341     }
342     Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
343     Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
344     return defaultSize;
345   }
346 
findSettableValue(String name, Collection<String> supportedValues, String... desiredValues)347   private static String findSettableValue(String name,
348                                           Collection<String> supportedValues,
349                                           String... desiredValues) {
350     Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues));
351     Log.i(TAG, "Supported " + name + " values: " + supportedValues);
352     if (supportedValues != null) {
353       for (String desiredValue : desiredValues) {
354         if (supportedValues.contains(desiredValue)) {
355           Log.i(TAG, "Can set " + name + " to: " + desiredValue);
356           return desiredValue;
357         }
358       }
359     }
360     Log.i(TAG, "No supported values match");
361     return null;
362   }
363 
toString(Collection<int[]> arrays)364   private static String toString(Collection<int[]> arrays) {
365     if (arrays == null || arrays.isEmpty()) {
366       return "[]";
367     }
368     StringBuilder buffer = new StringBuilder();
369     buffer.append('[');
370     Iterator<int[]> it = arrays.iterator();
371     while (it.hasNext()) {
372       buffer.append(Arrays.toString(it.next()));
373       if (it.hasNext()) {
374         buffer.append(", ");
375       }
376     }
377     buffer.append(']');
378     return buffer.toString();
379   }
380 
toString(Iterable<Camera.Area> areas)381   private static String toString(Iterable<Camera.Area> areas) {
382     if (areas == null) {
383       return null;
384     }
385     StringBuilder result = new StringBuilder();
386     for (Camera.Area area : areas) {
387       result.append(area.rect).append(':').append(area.weight).append(' ');
388     }
389     return result.toString();
390   }
391 
collectStats(Camera.Parameters parameters)392   public static String collectStats(Camera.Parameters parameters) {
393     return collectStats(parameters.flatten());
394   }
395 
collectStats(CharSequence flattenedParams)396   public static String collectStats(CharSequence flattenedParams) {
397     StringBuilder result = new StringBuilder(1000);
398     appendStat(result, "BOARD", Build.BOARD);
399     appendStat(result, "BRAND", Build.BRAND);
400     appendStat(result, "CPU_ABI", Build.CPU_ABI);
401     appendStat(result, "DEVICE", Build.DEVICE);
402     appendStat(result, "DISPLAY", Build.DISPLAY);
403     appendStat(result, "FINGERPRINT", Build.FINGERPRINT);
404     appendStat(result, "HOST", Build.HOST);
405     appendStat(result, "ID", Build.ID);
406     appendStat(result, "MANUFACTURER", Build.MANUFACTURER);
407     appendStat(result, "MODEL", Build.MODEL);
408     appendStat(result, "PRODUCT", Build.PRODUCT);
409     appendStat(result, "TAGS", Build.TAGS);
410     appendStat(result, "TIME", Build.TIME);
411     appendStat(result, "TYPE", Build.TYPE);
412     appendStat(result, "USER", Build.USER);
413     appendStat(result, "VERSION.CODENAME", Build.VERSION.CODENAME);
414     appendStat(result, "VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL);
415     appendStat(result, "VERSION.RELEASE", Build.VERSION.RELEASE);
416     appendStat(result, "VERSION.SDK_INT", Build.VERSION.SDK_INT);
417 
418     if (flattenedParams != null) {
419       String[] params = SEMICOLON.split(flattenedParams);
420       Arrays.sort(params);
421       for (String param : params) {
422         result.append(param).append('\n');
423       }
424     }
425 
426     return result.toString();
427   }
428 
appendStat(StringBuilder builder, String stat, Object value)429   private static void appendStat(StringBuilder builder, String stat, Object value) {
430     builder.append(stat).append('=').append(value).append('\n');
431   }
432 
433 }
434