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;
28import com.google.zxing.PlanarYUVLuminanceSource;
29import eu.siacs.conversations.Config;
30import java.io.IOException;
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.Collections;
34import java.util.Comparator;
35import java.util.List;
36
37/**
38 * @author Andreas Schildbach
39 */
40@SuppressWarnings("deprecation")
41public final class CameraManager {
42 private static final int MIN_FRAME_SIZE = 240;
43 private static final int MAX_FRAME_SIZE = 600;
44 private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen
45 private static final int MAX_PREVIEW_PIXELS = 1280 * 720;
46
47 private Camera camera;
48 private final CameraInfo cameraInfo = new CameraInfo();
49 private Camera.Size cameraResolution;
50 private Rect frame;
51 private RectF framePreview;
52
53 public Rect getFrame() {
54 return frame;
55 }
56
57 public RectF getFramePreview() {
58 return framePreview;
59 }
60
61 public int getFacing() {
62 return cameraInfo.facing;
63 }
64
65 public int getOrientation() {
66 return cameraInfo.orientation;
67 }
68
69 public Camera open(
70 final TextureView textureView,
71 final int displayOrientation,
72 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 throw new IllegalStateException("facing: " + cameraInfo.facing);
84
85 camera.setPreviewTexture(textureView.getSurfaceTexture());
86
87 final int width = textureView.getWidth();
88 final int height = textureView.getHeight();
89
90 final Camera.Parameters parameters = camera.getParameters();
91
92 cameraResolution = findBestPreviewSizeValue(parameters, width, height);
93
94 final int rawSize = Math.min(width * 2 / 3, height * 2 / 3);
95 final int frameSize = Math.max(MIN_FRAME_SIZE, Math.min(MAX_FRAME_SIZE, rawSize));
96
97 final int leftOffset = (width - frameSize) / 2;
98 final int topOffset = (height - frameSize) / 2;
99 frame = new Rect(leftOffset, topOffset, leftOffset + frameSize, topOffset + frameSize);
100
101 float widthFactor;
102 float heightFactor;
103 Rect orientedFrame;
104 boolean isTexturePortrait = width < height;
105 boolean isCameraPortrait = cameraResolution.width < cameraResolution.height;
106 if (isTexturePortrait == isCameraPortrait) {
107 widthFactor = (float) cameraResolution.width / width;
108 heightFactor = (float) cameraResolution.height / height;
109 orientedFrame = new Rect(frame);
110 } else {
111 widthFactor = (float) cameraResolution.width / height;
112 heightFactor = (float) cameraResolution.height / width;
113 // Swap X and Y coordinates to flip frame to the same orientation as cameraResolution
114 orientedFrame = new Rect(frame.top, frame.left, frame.bottom, frame.right);
115 }
116
117 framePreview =
118 new RectF(
119 orientedFrame.left * widthFactor,
120 orientedFrame.top * heightFactor,
121 orientedFrame.right * widthFactor,
122 orientedFrame.bottom * heightFactor);
123
124 final String savedParameters = parameters == null ? null : parameters.flatten();
125
126 try {
127 setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
128 } catch (final RuntimeException x) {
129 if (savedParameters != null) {
130 final Camera.Parameters parameters2 = camera.getParameters();
131 parameters2.unflatten(savedParameters);
132 try {
133 camera.setParameters(parameters2);
134 setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
135 } catch (final RuntimeException x2) {
136 Log.d(Config.LOGTAG, "problem setting camera parameters", x2);
137 }
138 }
139 }
140
141 try {
142 camera.startPreview();
143 return camera;
144 } catch (final RuntimeException x) {
145 Log.w(Config.LOGTAG, "something went wrong while starting camera preview", x);
146 camera.release();
147 throw x;
148 }
149 }
150
151 private int determineCameraId() {
152 final int cameraCount = Camera.getNumberOfCameras();
153 final CameraInfo cameraInfo = new CameraInfo();
154
155 // prefer back-facing camera
156 for (int i = 0; i < cameraCount; i++) {
157 Camera.getCameraInfo(i, cameraInfo);
158 if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) return i;
159 }
160
161 // fall back to front-facing camera
162 for (int i = 0; i < cameraCount; i++) {
163 Camera.getCameraInfo(i, cameraInfo);
164 if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) return i;
165 }
166
167 return -1;
168 }
169
170 public void close() {
171 if (camera != null) {
172 try {
173 camera.stopPreview();
174 } catch (final RuntimeException x) {
175 Log.w(Config.LOGTAG, "something went wrong while stopping camera preview", x);
176 }
177
178 camera.release();
179 }
180 }
181
182 private static final Comparator<Camera.Size> numPixelComparator =
183 new Comparator<Camera.Size>() {
184 @Override
185 public int compare(final Camera.Size size1, final Camera.Size size2) {
186 final int pixels1 = size1.height * size1.width;
187 final int pixels2 = size2.height * size2.width;
188
189 if (pixels1 < pixels2) return 1;
190 else if (pixels1 > pixels2) return -1;
191 else return 0;
192 }
193 };
194
195 private static Camera.Size findBestPreviewSizeValue(
196 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) return parameters.getPreviewSize();
207
208 // sort by size, descending
209 final List<Camera.Size> supportedPreviewSizes =
210 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) continue;
221
222 final boolean isCandidatePortrait = realWidth < realHeight;
223 final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
224 final int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
225 if (maybeFlippedWidth == width && maybeFlippedHeight == height)
226 return supportedPreviewSize;
227
228 final float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight;
229 final float newDiff = Math.abs(aspectRatio - screenAspectRatio);
230 if (newDiff < diff) {
231 bestSize = supportedPreviewSize;
232 diff = newDiff;
233 }
234 }
235
236 if (bestSize != null) return bestSize;
237 else return parameters.getPreviewSize();
238 }
239
240 @SuppressLint("InlinedApi")
241 private static void setDesiredCameraParameters(
242 final Camera camera,
243 final Camera.Size cameraResolution,
244 final boolean continuousAutoFocus) {
245 final Camera.Parameters parameters = camera.getParameters();
246 if (parameters == null) return;
247
248 final List<String> supportedFocusModes = parameters.getSupportedFocusModes();
249 final String focusMode =
250 continuousAutoFocus
251 ? findValue(
252 supportedFocusModes,
253 Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
254 Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
255 Camera.Parameters.FOCUS_MODE_AUTO,
256 Camera.Parameters.FOCUS_MODE_MACRO)
257 : findValue(
258 supportedFocusModes,
259 Camera.Parameters.FOCUS_MODE_AUTO,
260 Camera.Parameters.FOCUS_MODE_MACRO);
261 if (focusMode != null) parameters.setFocusMode(focusMode);
262
263 parameters.setPreviewSize(cameraResolution.width, cameraResolution.height);
264
265 camera.setParameters(parameters);
266 }
267
268 public void requestPreviewFrame(final PreviewCallback callback) {
269 try {
270 camera.setOneShotPreviewCallback(callback);
271 } catch (final RuntimeException x) {
272 Log.d(Config.LOGTAG, "problem requesting preview frame, callback won't be called", x);
273 }
274 }
275
276 public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) {
277 return new PlanarYUVLuminanceSource(
278 data,
279 cameraResolution.width,
280 cameraResolution.height,
281 (int) framePreview.left,
282 (int) framePreview.top,
283 (int) framePreview.width(),
284 (int) framePreview.height(),
285 false);
286 }
287
288 public void setTorch(final boolean enabled) {
289 if (enabled != getTorchEnabled(camera)) setTorchEnabled(camera, enabled);
290 }
291
292 private static boolean getTorchEnabled(final Camera camera) {
293 final Camera.Parameters parameters = camera.getParameters();
294 if (parameters != null) {
295 final String flashMode = camera.getParameters().getFlashMode();
296 return (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)
297 || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode));
298 }
299
300 return false;
301 }
302
303 private static void setTorchEnabled(final Camera camera, final boolean enabled) {
304 final Camera.Parameters parameters = camera.getParameters();
305
306 final List<String> supportedFlashModes = parameters.getSupportedFlashModes();
307 if (supportedFlashModes != null) {
308 final String flashMode;
309 if (enabled)
310 flashMode =
311 findValue(
312 supportedFlashModes,
313 Camera.Parameters.FLASH_MODE_TORCH,
314 Camera.Parameters.FLASH_MODE_ON);
315 else flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF);
316
317 if (flashMode != null) {
318 camera.cancelAutoFocus(); // autofocus can cause conflict
319
320 parameters.setFlashMode(flashMode);
321 camera.setParameters(parameters);
322 }
323 }
324 }
325
326 private static String findValue(final Collection<String> values, final String... valuesToFind) {
327 for (final String valueToFind : valuesToFind)
328 if (values.contains(valueToFind)) return valueToFind;
329
330 return null;
331 }
332}