kopia lustrzana https://github.com/ryukoposting/Signal-Android
473 wiersze
14 KiB
Java
473 wiersze
14 KiB
Java
/*
|
|
Copyright (c) 2013-2014 CommonsWare, LLC
|
|
Portions Copyright (C) 2007 The Android Open Source Project
|
|
<p>
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
not use this file except in compliance with the License. You may obtain
|
|
a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package org.signal.qr.kitkat;
|
|
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.content.pm.ActivityInfo;
|
|
import android.graphics.Color;
|
|
import android.hardware.Camera;
|
|
import android.hardware.Camera.CameraInfo;
|
|
import android.hardware.Camera.Parameters;
|
|
import android.hardware.Camera.Size;
|
|
import android.os.AsyncTask;
|
|
import android.util.AttributeSet;
|
|
import android.view.OrientationEventListener;
|
|
import android.view.ViewGroup;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.signal.core.util.ThreadUtil;
|
|
import org.signal.core.util.logging.Log;
|
|
|
|
import java.util.Collections;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
@SuppressWarnings("deprecation")
|
|
public class QrCameraView extends ViewGroup {
|
|
private static final String TAG = Log.tag(QrCameraView.class);
|
|
|
|
private final CameraSurfaceView surface;
|
|
private final OnOrientationChange onOrientationChange;
|
|
|
|
private volatile Optional<Camera> camera = Optional.empty();
|
|
private final int cameraId = CameraInfo.CAMERA_FACING_BACK;
|
|
private volatile int displayOrientation = -1;
|
|
|
|
private @NonNull State state = State.PAUSED;
|
|
private @Nullable Size previewSize;
|
|
private final List<CameraViewListener> listeners = Collections.synchronizedList(new LinkedList<>());
|
|
private int outputOrientation = -1;
|
|
|
|
public QrCameraView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public QrCameraView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public QrCameraView(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
setBackgroundColor(Color.BLACK);
|
|
|
|
surface = new CameraSurfaceView(getContext());
|
|
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
|
|
addView(surface);
|
|
}
|
|
|
|
public void onResume() {
|
|
if (state != State.PAUSED) return;
|
|
state = State.RESUMED;
|
|
Log.i(TAG, "onResume() queued");
|
|
enqueueTask(new SerialAsyncTask<Void>() {
|
|
@Override
|
|
protected
|
|
@Nullable
|
|
Void onRunBackground() {
|
|
try {
|
|
long openStartMillis = System.currentTimeMillis();
|
|
camera = Optional.ofNullable(Camera.open(cameraId));
|
|
Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms");
|
|
synchronized (QrCameraView.this) {
|
|
QrCameraView.this.notifyAll();
|
|
}
|
|
camera.ifPresent(value -> onCameraReady(value));
|
|
} catch (Exception e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostMain(Void avoid) {
|
|
if (!camera.isPresent()) {
|
|
Log.w(TAG, "tried to open camera but got null");
|
|
for (CameraViewListener listener : listeners) {
|
|
listener.onCameraFail();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
|
onOrientationChange.enable();
|
|
}
|
|
Log.i(TAG, "onResume() completed");
|
|
}
|
|
});
|
|
}
|
|
|
|
public void onPause() {
|
|
if (state == State.PAUSED) return;
|
|
state = State.PAUSED;
|
|
Log.i(TAG, "onPause() queued");
|
|
|
|
enqueueTask(new SerialAsyncTask<Void>() {
|
|
private Optional<Camera> cameraToDestroy;
|
|
|
|
@Override
|
|
protected void onPreMain() {
|
|
cameraToDestroy = camera;
|
|
camera = Optional.empty();
|
|
}
|
|
|
|
@Override
|
|
protected Void onRunBackground() {
|
|
if (cameraToDestroy.isPresent()) {
|
|
try {
|
|
stopPreview();
|
|
cameraToDestroy.get().setPreviewCallback(null);
|
|
cameraToDestroy.get().release();
|
|
Log.w(TAG, "released old camera instance");
|
|
} catch (Exception e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override protected void onPostMain(Void avoid) {
|
|
onOrientationChange.disable();
|
|
displayOrientation = -1;
|
|
outputOrientation = -1;
|
|
removeView(surface);
|
|
addView(surface);
|
|
Log.i(TAG, "onPause() completed");
|
|
}
|
|
});
|
|
|
|
for (CameraViewListener listener : listeners) {
|
|
listener.onCameraStop();
|
|
}
|
|
}
|
|
|
|
public boolean isStarted() {
|
|
return state != State.PAUSED;
|
|
}
|
|
|
|
@SuppressWarnings("SuspiciousNameCombination")
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
final int width = r - l;
|
|
final int height = b - t;
|
|
final int previewWidth;
|
|
final int previewHeight;
|
|
|
|
if (camera.isPresent() && previewSize != null) {
|
|
if (displayOrientation == 90 || displayOrientation == 270) {
|
|
previewWidth = previewSize.height;
|
|
previewHeight = previewSize.width;
|
|
} else {
|
|
previewWidth = previewSize.width;
|
|
previewHeight = previewSize.height;
|
|
}
|
|
} else {
|
|
previewWidth = width;
|
|
previewHeight = height;
|
|
}
|
|
|
|
if (previewHeight == 0 || previewWidth == 0) {
|
|
Log.w(TAG, "skipping layout due to zero-width/height preview size");
|
|
return;
|
|
}
|
|
|
|
if (width * previewHeight > height * previewWidth) {
|
|
final int scaledChildHeight = previewHeight * width / previewWidth;
|
|
surface.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2);
|
|
} else {
|
|
final int scaledChildWidth = previewWidth * height / previewHeight;
|
|
surface.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
Log.i(TAG, "onSizeChanged(" + oldw + "x" + oldh + " -> " + w + "x" + h + ")");
|
|
super.onSizeChanged(w, h, oldw, oldh);
|
|
camera.ifPresent(value -> startPreview(value.getParameters()));
|
|
}
|
|
|
|
public void addListener(@NonNull CameraViewListener listener) {
|
|
listeners.add(listener);
|
|
}
|
|
|
|
public void setPreviewCallback(final @NonNull PreviewCallback previewCallback) {
|
|
enqueueTask(new PostInitializationTask<Void>() {
|
|
@Override
|
|
protected void onPostMain(Void avoid) {
|
|
camera.ifPresent(value -> value.setPreviewCallback((data, camera) -> {
|
|
if (!QrCameraView.this.camera.isPresent()) {
|
|
return;
|
|
}
|
|
|
|
final int rotation = getCameraPictureOrientation();
|
|
final Size previewSize = camera.getParameters().getPreviewSize();
|
|
if (data != null) {
|
|
previewCallback.onPreviewFrame(new PreviewFrame(data, previewSize.width, previewSize.height, rotation));
|
|
}
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
|
|
private void onCameraReady(final @NonNull Camera camera) {
|
|
final Parameters parameters = camera.getParameters();
|
|
|
|
parameters.setRecordingHint(true);
|
|
final List<String> focusModes = parameters.getSupportedFocusModes();
|
|
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
|
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
|
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
|
}
|
|
|
|
displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo());
|
|
camera.setDisplayOrientation(displayOrientation);
|
|
camera.setParameters(parameters);
|
|
enqueueTask(new PostInitializationTask<Void>() {
|
|
@Override
|
|
protected Void onRunBackground() {
|
|
try {
|
|
camera.setPreviewDisplay(surface.getHolder());
|
|
startPreview(parameters);
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "couldn't set preview display", e);
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void startPreview(final @NonNull Parameters parameters) {
|
|
camera.ifPresent(camera -> {
|
|
try {
|
|
final Size preferredPreviewSize = getPreferredPreviewSize(parameters);
|
|
|
|
if (preferredPreviewSize != null && !parameters.getPreviewSize().equals(preferredPreviewSize)) {
|
|
Log.i(TAG, "starting preview with size " + preferredPreviewSize.width + "x" + preferredPreviewSize.height);
|
|
if (state == State.ACTIVE) stopPreview();
|
|
previewSize = preferredPreviewSize;
|
|
parameters.setPreviewSize(preferredPreviewSize.width, preferredPreviewSize.height);
|
|
camera.setParameters(parameters);
|
|
} else {
|
|
previewSize = parameters.getPreviewSize();
|
|
}
|
|
long previewStartMillis = System.currentTimeMillis();
|
|
camera.startPreview();
|
|
Log.i(TAG, "camera.startPreview() -> " + (System.currentTimeMillis() - previewStartMillis) + "ms");
|
|
state = State.ACTIVE;
|
|
ThreadUtil.runOnMain(() -> {
|
|
requestLayout();
|
|
for (CameraViewListener listener : listeners) {
|
|
listener.onCameraStart();
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void stopPreview() {
|
|
camera.ifPresent(camera -> {
|
|
try {
|
|
camera.stopPreview();
|
|
state = State.RESUMED;
|
|
} catch (Exception e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private Size getPreferredPreviewSize(@NonNull Parameters parameters) {
|
|
return CameraUtils.getPreferredPreviewSize(displayOrientation,
|
|
getMeasuredWidth(),
|
|
getMeasuredHeight(),
|
|
parameters);
|
|
}
|
|
|
|
private int getCameraPictureOrientation() {
|
|
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
|
outputOrientation = getCameraPictureRotation(getActivity().getWindowManager()
|
|
.getDefaultDisplay()
|
|
.getOrientation());
|
|
} else if (getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT) {
|
|
outputOrientation = (360 - displayOrientation) % 360;
|
|
} else {
|
|
outputOrientation = displayOrientation;
|
|
}
|
|
|
|
return outputOrientation;
|
|
}
|
|
|
|
private @NonNull CameraInfo getCameraInfo() {
|
|
final CameraInfo info = new CameraInfo();
|
|
Camera.getCameraInfo(cameraId, info);
|
|
return info;
|
|
}
|
|
|
|
// XXX this sucks
|
|
private Activity getActivity() {
|
|
return (Activity) getContext();
|
|
}
|
|
|
|
public int getCameraPictureRotation(int orientation) {
|
|
final CameraInfo info = getCameraInfo();
|
|
final int rotation;
|
|
|
|
orientation = (orientation + 45) / 90 * 90;
|
|
|
|
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
|
|
rotation = (info.orientation - orientation + 360) % 360;
|
|
} else {
|
|
rotation = (info.orientation + orientation) % 360;
|
|
}
|
|
|
|
return rotation;
|
|
}
|
|
|
|
private class OnOrientationChange extends OrientationEventListener {
|
|
public OnOrientationChange(Context context) {
|
|
super(context);
|
|
disable();
|
|
}
|
|
|
|
@Override
|
|
public void onOrientationChanged(int orientation) {
|
|
camera.ifPresent(camera -> {
|
|
if (orientation != ORIENTATION_UNKNOWN) {
|
|
int newOutputOrientation = getCameraPictureRotation(orientation);
|
|
|
|
if (newOutputOrientation != outputOrientation) {
|
|
outputOrientation = newOutputOrientation;
|
|
|
|
Parameters params = camera.getParameters();
|
|
|
|
params.setRotation(outputOrientation);
|
|
|
|
try {
|
|
camera.setParameters(params);
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void enqueueTask(SerialAsyncTask<?> job) {
|
|
AsyncTask.SERIAL_EXECUTOR.execute(job);
|
|
}
|
|
|
|
public static abstract class SerialAsyncTask<Result> implements Runnable {
|
|
|
|
@Override
|
|
public final void run() {
|
|
if (!onWait()) {
|
|
Log.w(TAG, "skipping task, preconditions not met in onWait()");
|
|
return;
|
|
}
|
|
|
|
ThreadUtil.runOnMainSync(this::onPreMain);
|
|
final Result result = onRunBackground();
|
|
ThreadUtil.runOnMainSync(() -> onPostMain(result));
|
|
}
|
|
|
|
protected boolean onWait() {return true;}
|
|
|
|
protected void onPreMain() {}
|
|
|
|
protected Result onRunBackground() {return null;}
|
|
|
|
protected void onPostMain(Result result) {}
|
|
}
|
|
|
|
private abstract class PostInitializationTask<Result> extends SerialAsyncTask<Result> {
|
|
@Override protected boolean onWait() {
|
|
synchronized (QrCameraView.this) {
|
|
if (!camera.isPresent()) {
|
|
return false;
|
|
}
|
|
while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) {
|
|
Log.i(TAG, String.format("waiting. surface ready? %s", surface.isReady()));
|
|
waitFor();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void waitFor() {
|
|
try {
|
|
wait(0);
|
|
} catch (InterruptedException ie) {
|
|
throw new AssertionError(ie);
|
|
}
|
|
}
|
|
|
|
public interface CameraViewListener {
|
|
void onImageCapture(@NonNull final byte[] imageBytes);
|
|
|
|
void onCameraFail();
|
|
|
|
void onCameraStart();
|
|
|
|
void onCameraStop();
|
|
}
|
|
|
|
public interface PreviewCallback {
|
|
void onPreviewFrame(@NonNull PreviewFrame frame);
|
|
}
|
|
|
|
public static class PreviewFrame {
|
|
private final @NonNull byte[] data;
|
|
private final int width;
|
|
private final int height;
|
|
private final int orientation;
|
|
|
|
public PreviewFrame(@NonNull byte[] data, int width, int height, int orientation) {
|
|
this.data = data;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.orientation = orientation;
|
|
}
|
|
|
|
public @NonNull byte[] getData() {
|
|
return data;
|
|
}
|
|
|
|
public int getWidth() {
|
|
return width;
|
|
}
|
|
|
|
public int getHeight() {
|
|
return height;
|
|
}
|
|
|
|
public int getOrientation() {
|
|
return orientation;
|
|
}
|
|
}
|
|
|
|
private enum State {
|
|
PAUSED, RESUMED, ACTIVE
|
|
}
|
|
}
|