1/*
2 * Copyright 2012-2015 the original author or authors.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18package eu.siacs.conversations.ui.service;
19
20import android.annotation.SuppressLint;
21import android.graphics.Rect;
22import android.graphics.RectF;
23import android.hardware.Camera;
24import android.hardware.Camera.CameraInfo;
25import android.hardware.Camera.PreviewCallback;
26import android.util.Log;
27import android.view.TextureView;
28
29import com.google.zxing.PlanarYUVLuminanceSource;
30
31import java.io.IOException;
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.Comparator;
36import java.util.List;
37
38import eu.siacs.conversations.Config;
39
40/**
41 * @author Andreas Schildbach
42 */
43@SuppressWarnings("deprecation")
44public final class CameraManager {
45 private static final int MIN_FRAME_SIZE = 240;
46 private static final int MAX_FRAME_SIZE = 600;
47 private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen
48 private static final int MAX_PREVIEW_PIXELS = 1280 * 720;
49
50 private Camera camera;
51 private final CameraInfo cameraInfo = new CameraInfo();
52 private Camera.Size cameraResolution;
53 private Rect frame;
54 private RectF framePreview;
55
56 public Rect getFrame() {
57 return frame;
58 }
59
60 public RectF getFramePreview() {
61 return framePreview;
62 }
63
64 public int getFacing() {
65 return cameraInfo.facing;
66 }
67
68 public int getOrientation() {
69 return cameraInfo.orientation;
70 }
71
72 public Camera open(final TextureView textureView, final int displayOrientation, final boolean continuousAutoFocus)
73 throws IOException {
74 final int cameraId = determineCameraId();
75 Camera.getCameraInfo(cameraId, cameraInfo);
76
77 camera = Camera.open(cameraId);
78
79 if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
80 camera.setDisplayOrientation((720 - displayOrientation - cameraInfo.orientation) % 360);
81 else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
82 camera.setDisplayOrientation((720 - displayOrientation + cameraInfo.orientation) % 360);
83 else
84 throw new IllegalStateException("facing: " + cameraInfo.facing);
85
86 camera.setPreviewTexture(textureView.getSurfaceTexture());
87
88 final int width = textureView.getWidth();
89 final int height = textureView.getHeight();
90
91 final Camera.Parameters parameters = camera.getParameters();
92
93 cameraResolution = findBestPreviewSizeValue(parameters, width, height);
94
95 final int rawSize = Math.min(width * 2 / 3, height * 2 / 3);
96 final int frameSize = Math.max(MIN_FRAME_SIZE, Math.min(MAX_FRAME_SIZE, rawSize));
97
98 final int leftOffset = (width - frameSize) / 2;
99 final int topOffset = (height - frameSize) / 2;
100 frame = new Rect(leftOffset, topOffset, leftOffset + frameSize, topOffset + frameSize);
101
102 float widthFactor;
103 float heightFactor;
104 Rect orientedFrame;
105 boolean isTexturePortrait = width < height;
106 boolean isCameraPortrait = cameraResolution.width < cameraResolution.height;
107 if (isTexturePortrait == isCameraPortrait) {
108 widthFactor = (float)cameraResolution.width / width;
109 heightFactor = (float)cameraResolution.height / height;
110 orientedFrame = new Rect(frame);
111 } else {
112 widthFactor = (float)cameraResolution.width / height;
113 heightFactor = (float)cameraResolution.height / width;
114 // Swap X and Y coordinates to flip frame to the same orientation as cameraResolution
115 orientedFrame = new Rect(frame.top, frame.left, frame.bottom, frame.right);
116 }
117
118 framePreview = new RectF(orientedFrame.left * widthFactor, orientedFrame.top * heightFactor,
119 orientedFrame.right * widthFactor, orientedFrame.bottom * heightFactor);
120
121 final String savedParameters = parameters == null ? null : parameters.flatten();
122
123 try {
124 setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
125 } catch (final RuntimeException x) {
126 if (savedParameters != null) {
127 final Camera.Parameters parameters2 = camera.getParameters();
128 parameters2.unflatten(savedParameters);
129 try {
130 camera.setParameters(parameters2);
131 setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
132 } catch (final RuntimeException x2) {
133 Log.d(Config.LOGTAG,"problem setting camera parameters", x2);
134 }
135 }
136 }
137
138 try {
139 camera.startPreview();
140 return camera;
141 } catch (final RuntimeException x) {
142 Log.w(Config.LOGTAG,"something went wrong while starting camera preview", x);
143 camera.release();
144 throw x;
145 }
146 }
147
148 private int determineCameraId() {
149 final int cameraCount = Camera.getNumberOfCameras();
150 final CameraInfo cameraInfo = new CameraInfo();
151
152 // prefer back-facing camera
153 for (int i = 0; i < cameraCount; i++) {
154 Camera.getCameraInfo(i, cameraInfo);
155 if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
156 return i;
157 }
158
159 // fall back to front-facing camera
160 for (int i = 0; i < cameraCount; i++) {
161 Camera.getCameraInfo(i, cameraInfo);
162 if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
163 return i;
164 }
165
166 return -1;
167 }
168
169 public void close() {
170 if (camera != null) {
171 try {
172 camera.stopPreview();
173 } catch (final RuntimeException x) {
174 Log.w(Config.LOGTAG,"something went wrong while stopping camera preview", x);
175 }
176
177 camera.release();
178 }
179 }
180
181 private static final Comparator<Camera.Size> numPixelComparator = new Comparator<Camera.Size>() {
182 @Override
183 public int compare(final Camera.Size size1, final Camera.Size size2) {
184 final int pixels1 = size1.height * size1.width;
185 final int pixels2 = size2.height * size2.width;
186
187 if (pixels1 < pixels2)
188 return 1;
189 else if (pixels1 > pixels2)
190 return -1;
191 else
192 return 0;
193 }
194 };
195
196 private static Camera.Size findBestPreviewSizeValue(final Camera.Parameters parameters, int width, int height) {
197 if (height > width) {
198 final int temp = width;
199 width = height;
200 height = temp;
201 }
202
203 final float screenAspectRatio = (float) width / (float) height;
204
205 final List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
206 if (rawSupportedSizes == null)
207 return parameters.getPreviewSize();
208
209 // sort by size, descending
210 final List<Camera.Size> supportedPreviewSizes = new ArrayList<Camera.Size>(rawSupportedSizes);
211 Collections.sort(supportedPreviewSizes, numPixelComparator);
212
213 Camera.Size bestSize = null;
214 float diff = Float.POSITIVE_INFINITY;
215
216 for (final Camera.Size supportedPreviewSize : supportedPreviewSizes) {
217 final int realWidth = supportedPreviewSize.width;
218 final int realHeight = supportedPreviewSize.height;
219 final int realPixels = realWidth * realHeight;
220 if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS)
221 continue;
222
223 final boolean isCandidatePortrait = realWidth < realHeight;
224 final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
225 final int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
226 if (maybeFlippedWidth == width && maybeFlippedHeight == height)
227 return supportedPreviewSize;
228
229 final float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight;
230 final float newDiff = Math.abs(aspectRatio - screenAspectRatio);
231 if (newDiff < diff) {
232 bestSize = supportedPreviewSize;
233 diff = newDiff;
234 }
235 }
236
237 if (bestSize != null)
238 return bestSize;
239 else
240 return parameters.getPreviewSize();
241 }
242
243 @SuppressLint("InlinedApi")
244 private static void setDesiredCameraParameters(final Camera camera, final Camera.Size cameraResolution,
245 final boolean continuousAutoFocus) {
246 final Camera.Parameters parameters = camera.getParameters();
247 if (parameters == null)
248 return;
249
250 final List<String> supportedFocusModes = parameters.getSupportedFocusModes();
251 final String focusMode = continuousAutoFocus
252 ? findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
253 Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, Camera.Parameters.FOCUS_MODE_AUTO,
254 Camera.Parameters.FOCUS_MODE_MACRO)
255 : findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_MACRO);
256 if (focusMode != null)
257 parameters.setFocusMode(focusMode);
258
259 parameters.setPreviewSize(cameraResolution.width, cameraResolution.height);
260
261 camera.setParameters(parameters);
262 }
263
264 public void requestPreviewFrame(final PreviewCallback callback) {
265 try {
266 camera.setOneShotPreviewCallback(callback);
267 } catch (final RuntimeException x) {
268 Log.d(Config.LOGTAG,"problem requesting preview frame, callback won't be called", x);
269 }
270 }
271
272 public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) {
273 return new PlanarYUVLuminanceSource(data, cameraResolution.width, cameraResolution.height,
274 (int) framePreview.left, (int) framePreview.top, (int) framePreview.width(),
275 (int) framePreview.height(), false);
276 }
277
278 public void setTorch(final boolean enabled) {
279 if (enabled != getTorchEnabled(camera))
280 setTorchEnabled(camera, enabled);
281 }
282
283 private static boolean getTorchEnabled(final Camera camera) {
284 final Camera.Parameters parameters = camera.getParameters();
285 if (parameters != null) {
286 final String flashMode = camera.getParameters().getFlashMode();
287 return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)
288 || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode));
289 }
290
291 return false;
292 }
293
294 private static void setTorchEnabled(final Camera camera, final boolean enabled) {
295 final Camera.Parameters parameters = camera.getParameters();
296
297 final List<String> supportedFlashModes = parameters.getSupportedFlashModes();
298 if (supportedFlashModes != null) {
299 final String flashMode;
300 if (enabled)
301 flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH,
302 Camera.Parameters.FLASH_MODE_ON);
303 else
304 flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF);
305
306 if (flashMode != null) {
307 camera.cancelAutoFocus(); // autofocus can cause conflict
308
309 parameters.setFlashMode(flashMode);
310 camera.setParameters(parameters);
311 }
312 }
313 }
314
315 private static String findValue(final Collection<String> values, final String... valuesToFind) {
316 for (final String valueToFind : valuesToFind)
317 if (values.contains(valueToFind))
318 return valueToFind;
319
320 return null;
321 }
322}