Migrate all QR scanning to new scanner.
This commit is contained in:
parent
caf1329005
commit
18eac51576
11 changed files with 46 additions and 852 deletions
|
@ -57,7 +57,7 @@ public class DeviceAddFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
|
||||
scannerView.start(getViewLifecycleOwner(), FeatureFlags.useQrLegacyScan());
|
||||
scannerView.start(getViewLifecycleOwner());
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.camera;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
|
||||
private boolean ready;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public CameraSurfaceView(Context context) {
|
||||
super(context);
|
||||
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
|
||||
getHolder().addCallback(this);
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
ready = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
ready = false;
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.camera;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.hardware.Camera;
|
||||
import android.hardware.Camera.CameraInfo;
|
||||
import android.hardware.Camera.Parameters;
|
||||
import android.hardware.Camera.Size;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class CameraUtils {
|
||||
private static final String TAG = Log.tag(CameraUtils.class);
|
||||
/*
|
||||
* modified from: https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java
|
||||
*/
|
||||
public static @Nullable Size getPreferredPreviewSize(int displayOrientation,
|
||||
int width,
|
||||
int height,
|
||||
@NonNull Parameters parameters) {
|
||||
final int targetWidth = displayOrientation % 180 == 90 ? height : width;
|
||||
final int targetHeight = displayOrientation % 180 == 90 ? width : height;
|
||||
final double targetRatio = (double) targetWidth / targetHeight;
|
||||
|
||||
Log.d(TAG, String.format(Locale.US,
|
||||
"getPreferredPreviewSize(%d, %d, %d) -> target %dx%d, AR %.02f",
|
||||
displayOrientation, width, height,
|
||||
targetWidth, targetHeight, targetRatio));
|
||||
|
||||
List<Size> sizes = parameters.getSupportedPreviewSizes();
|
||||
List<Size> ideals = new LinkedList<>();
|
||||
List<Size> bigEnough = new LinkedList<>();
|
||||
|
||||
for (Size size : sizes) {
|
||||
Log.d(TAG, String.format(Locale.US, " %dx%d (%.02f)", size.width, size.height, (float)size.width / size.height));
|
||||
|
||||
if (size.height == size.width * targetRatio && size.height >= targetHeight && size.width >= targetWidth) {
|
||||
ideals.add(size);
|
||||
Log.d(TAG, " (ideal ratio)");
|
||||
} else if (size.width >= targetWidth && size.height >= targetHeight) {
|
||||
bigEnough.add(size);
|
||||
Log.d(TAG, " (good size, suboptimal ratio)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ideals.isEmpty()) return Collections.min(ideals, new AreaComparator());
|
||||
else if (!bigEnough.isEmpty()) return Collections.min(bigEnough, new AspectRatioComparator(targetRatio));
|
||||
else return Collections.max(sizes, new AreaComparator());
|
||||
}
|
||||
|
||||
// based on
|
||||
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
|
||||
// and http://stackoverflow.com/a/10383164/115145
|
||||
public static int getCameraDisplayOrientation(@NonNull Activity activity,
|
||||
@NonNull CameraInfo info)
|
||||
{
|
||||
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||
int degrees = 0;
|
||||
DisplayMetrics dm = new DisplayMetrics();
|
||||
|
||||
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
|
||||
|
||||
switch (rotation) {
|
||||
case Surface.ROTATION_0: degrees = 0; break;
|
||||
case Surface.ROTATION_90: degrees = 90; break;
|
||||
case Surface.ROTATION_180: degrees = 180; break;
|
||||
case Surface.ROTATION_270: degrees = 270; break;
|
||||
}
|
||||
|
||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
return (360 - ((info.orientation + degrees) % 360)) % 360;
|
||||
} else {
|
||||
return (info.orientation - degrees + 360) % 360;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AreaComparator implements Comparator<Size> {
|
||||
@Override
|
||||
public int compare(Size lhs, Size rhs) {
|
||||
return Long.signum(lhs.width * lhs.height - rhs.width * rhs.height);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AspectRatioComparator extends AreaComparator {
|
||||
private final double target;
|
||||
public AspectRatioComparator(double target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Size lhs, Size rhs) {
|
||||
final double lhsDiff = Math.abs(target - (double) lhs.width / lhs.height);
|
||||
final double rhsDiff = Math.abs(target - (double) rhs.width / rhs.height);
|
||||
if (lhsDiff < rhsDiff) return -1;
|
||||
else if (lhsDiff > rhsDiff) return 1;
|
||||
else return super.compare(lhs, rhs);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,575 +0,0 @@
|
|||
/***
|
||||
Copyright (c) 2013-2014 CommonsWare, LLC
|
||||
Portions Copyright (C) 2007 The Android Open Source Project
|
||||
|
||||
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.thoughtcrime.securesms.components.camera;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
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.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
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 org.signal.qr.kitkat.QrCameraView;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class CameraView extends ViewGroup {
|
||||
private static final String TAG = Log.tag(CameraView.class);
|
||||
|
||||
private final CameraSurfaceView surface;
|
||||
private final OnOrientationChange onOrientationChange;
|
||||
|
||||
private volatile Optional<Camera> camera = Optional.empty();
|
||||
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
|
||||
private volatile int displayOrientation = -1;
|
||||
|
||||
private @NonNull State state = State.PAUSED;
|
||||
private @Nullable Size previewSize;
|
||||
private @NonNull List<CameraViewListener> listeners = Collections.synchronizedList(new LinkedList<CameraViewListener>());
|
||||
private int outputOrientation = -1;
|
||||
|
||||
public CameraView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CameraView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CameraView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setBackgroundColor(Color.BLACK);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
int camera = typedArray.getInt(R.styleable.CameraView_camera, -1);
|
||||
|
||||
if (camera != -1) cameraId = camera;
|
||||
else if (isMultiCamera()) cameraId = TextSecurePreferences.getDirectCaptureCameraId(context);
|
||||
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
surface = new CameraSurfaceView(getContext());
|
||||
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
|
||||
addView(surface);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
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 (CameraView.this) {
|
||||
CameraView.this.notifyAll();
|
||||
}
|
||||
if (camera.isPresent()) onCameraReady(camera.get());
|
||||
} 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);
|
||||
if (camera.isPresent()) startPreview(camera.get().getParameters());
|
||||
}
|
||||
|
||||
public void addListener(@NonNull CameraViewListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void setPreviewCallback(final @NonNull QrCameraView.PreviewCallback previewCallback) {
|
||||
enqueueTask(new PostInitializationTask<Void>() {
|
||||
@Override
|
||||
protected void onPostMain(Void avoid) {
|
||||
if (camera.isPresent()) {
|
||||
camera.get().setPreviewCallback(new Camera.PreviewCallback() {
|
||||
@Override
|
||||
public void onPreviewFrame(byte[] data, Camera camera) {
|
||||
if (!CameraView.this.camera.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int rotation = getCameraPictureOrientation();
|
||||
final Size previewSize = camera.getParameters().getPreviewSize();
|
||||
if (data != null) {
|
||||
previewCallback.onPreviewFrame(new QrCameraView.PreviewFrame(data, previewSize.width, previewSize.height, rotation));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isMultiCamera() {
|
||||
return Camera.getNumberOfCameras() > 1;
|
||||
}
|
||||
|
||||
public boolean isRearCamera() {
|
||||
return cameraId == CameraInfo.CAMERA_FACING_BACK;
|
||||
}
|
||||
|
||||
public void flipCamera() {
|
||||
if (Camera.getNumberOfCameras() > 1) {
|
||||
cameraId = cameraId == CameraInfo.CAMERA_FACING_BACK
|
||||
? CameraInfo.CAMERA_FACING_FRONT
|
||||
: CameraInfo.CAMERA_FACING_BACK;
|
||||
onPause();
|
||||
onResume();
|
||||
TextSecurePreferences.setDirectCaptureCameraId(getContext(), cameraId);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(14)
|
||||
private void onCameraReady(final @NonNull Camera camera) {
|
||||
final Parameters parameters = camera.getParameters();
|
||||
|
||||
if (VERSION.SDK_INT >= 14) {
|
||||
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) {
|
||||
if (this.camera.isPresent()) {
|
||||
try {
|
||||
final Camera camera = this.camera.get();
|
||||
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(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestLayout();
|
||||
for (CameraViewListener listener : listeners) {
|
||||
listener.onCameraStart();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopPreview() {
|
||||
if (camera.isPresent()) {
|
||||
try {
|
||||
camera.get().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;
|
||||
}
|
||||
|
||||
// https://github.com/signalapp/Signal-Android/issues/4715
|
||||
private boolean isTroublemaker() {
|
||||
return getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT &&
|
||||
"JWR66Y".equals(Build.DISPLAY) &&
|
||||
"yakju".equals(Build.PRODUCT);
|
||||
}
|
||||
|
||||
private @NonNull CameraInfo getCameraInfo() {
|
||||
final CameraInfo info = new Camera.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 == Camera.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) {
|
||||
if (camera.isPresent() && orientation != ORIENTATION_UNKNOWN) {
|
||||
int newOutputOrientation = getCameraPictureRotation(orientation);
|
||||
|
||||
if (newOutputOrientation != outputOrientation) {
|
||||
outputOrientation = newOutputOrientation;
|
||||
|
||||
Camera.Parameters params = camera.get().getParameters();
|
||||
|
||||
params.setRotation(outputOrientation);
|
||||
|
||||
try {
|
||||
camera.get().setParameters(params);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void takePicture(final Rect previewRect) {
|
||||
if (!camera.isPresent() || camera.get().getParameters() == null) {
|
||||
Log.w(TAG, "camera not in capture-ready state");
|
||||
return;
|
||||
}
|
||||
|
||||
camera.get().setOneShotPreviewCallback(new Camera.PreviewCallback() {
|
||||
@Override
|
||||
public void onPreviewFrame(byte[] data, final Camera camera) {
|
||||
final int rotation = getCameraPictureOrientation();
|
||||
final Size previewSize = camera.getParameters().getPreviewSize();
|
||||
final Rect croppingRect = getCroppedRect(previewSize, previewRect, rotation);
|
||||
|
||||
Log.i(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height);
|
||||
Log.i(TAG, "data bytes: " + data.length);
|
||||
Log.i(TAG, "previewFormat: " + camera.getParameters().getPreviewFormat());
|
||||
Log.i(TAG, "croppingRect: " + croppingRect.toString());
|
||||
Log.i(TAG, "rotation: " + rotation);
|
||||
new CaptureTask(previewSize, rotation, croppingRect).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) {
|
||||
final int previewWidth = cameraPreviewSize.width;
|
||||
final int previewHeight = cameraPreviewSize.height;
|
||||
|
||||
if (rotation % 180 > 0) rotateRect(visibleRect);
|
||||
|
||||
float scale = (float) previewWidth / visibleRect.width();
|
||||
if (visibleRect.height() * scale > previewHeight) {
|
||||
scale = (float) previewHeight / visibleRect.height();
|
||||
}
|
||||
final float newWidth = visibleRect.width() * scale;
|
||||
final float newHeight = visibleRect.height() * scale;
|
||||
final float centerX = (VERSION.SDK_INT < 14 || isTroublemaker()) ? previewWidth - newWidth / 2 : previewWidth / 2;
|
||||
final float centerY = previewHeight / 2;
|
||||
|
||||
visibleRect.set((int) (centerX - newWidth / 2),
|
||||
(int) (centerY - newHeight / 2),
|
||||
(int) (centerX + newWidth / 2),
|
||||
(int) (centerY + newHeight / 2));
|
||||
|
||||
if (rotation % 180 > 0) rotateRect(visibleRect);
|
||||
return visibleRect;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void rotateRect(Rect rect) {
|
||||
rect.set(rect.top, rect.left, rect.bottom, rect.right);
|
||||
}
|
||||
|
||||
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 (CameraView.this) {
|
||||
if (!camera.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) {
|
||||
Log.i(TAG, String.format("waiting. surface ready? %s", surface.isReady()));
|
||||
Util.wait(CameraView.this, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CaptureTask extends AsyncTask<byte[], Void, byte[]> {
|
||||
private final Size previewSize;
|
||||
private final int rotation;
|
||||
private final Rect croppingRect;
|
||||
|
||||
public CaptureTask(Size previewSize, int rotation, Rect croppingRect) {
|
||||
this.previewSize = previewSize;
|
||||
this.rotation = rotation;
|
||||
this.croppingRect = croppingRect;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] doInBackground(byte[]... params) {
|
||||
final byte[] data = params[0];
|
||||
try {
|
||||
return BitmapUtil.createFromNV21(data,
|
||||
previewSize.width,
|
||||
previewSize.height,
|
||||
rotation,
|
||||
croppingRect,
|
||||
cameraId == CameraInfo.CAMERA_FACING_FRONT);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] imageBytes) {
|
||||
if (imageBytes != null) {
|
||||
for (CameraViewListener listener : listeners) {
|
||||
listener.onImageCapture(imageBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PreconditionsNotMetException extends Exception {}
|
||||
|
||||
public interface CameraViewListener {
|
||||
void onImageCapture(@NonNull final byte[] imageBytes);
|
||||
void onCameraFail();
|
||||
void onCameraStart();
|
||||
void onCameraStop();
|
||||
}
|
||||
|
||||
private enum State {
|
||||
PAUSED, RESUMED, ACTIVE
|
||||
}
|
||||
}
|
|
@ -11,22 +11,25 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.qr.kitkat.ScanningThread;
|
||||
import org.signal.qr.QrScannerView;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PaymentsTransferQrScanFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(PaymentsTransferQrScanFragment.class);
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
private LinearLayout overlay;
|
||||
private CameraView scannerView;
|
||||
private ScanningThread scanningThread;
|
||||
private QrScannerView scannerView;
|
||||
private PaymentsTransferViewModel viewModel;
|
||||
|
||||
public PaymentsTransferQrScanFragment() {
|
||||
|
@ -48,45 +51,35 @@ public final class PaymentsTransferQrScanFragment extends LoggingFragment {
|
|||
|
||||
Toolbar toolbar = view.findViewById(R.id.payments_transfer_scan_qr);
|
||||
toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(v).popBackStack());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
scanningThread = new ScanningThread();
|
||||
scanningThread.setScanListener(data -> ThreadUtil.runOnMain(() -> {
|
||||
try {
|
||||
viewModel.postQrData(MobileCoinPublicAddress.fromQr(data).getPaymentAddressBase58());
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_paymentsScanQr_pop);
|
||||
} catch (MobileCoinPublicAddress.AddressException e) {
|
||||
Log.e(TAG, "Not a valid address");
|
||||
}
|
||||
}));
|
||||
scannerView.onResume();
|
||||
scannerView.setPreviewCallback(scanningThread);
|
||||
scanningThread.start();
|
||||
}
|
||||
scannerView.start(getViewLifecycleOwner());
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
scannerView.onPause();
|
||||
scanningThread.stopScanning();
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
Disposable qrDisposable = scannerView
|
||||
.getQrData()
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(data -> {
|
||||
try {
|
||||
viewModel.postQrData(MobileCoinPublicAddress.fromQr(data).getPaymentAddressBase58());
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_paymentsScanQr_pop);
|
||||
} catch (MobileCoinPublicAddress.AddressException e) {
|
||||
Log.e(TAG, "Not a valid address");
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleDisposable.add(qrDisposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
|
||||
scannerView.onPause();
|
||||
|
||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
overlay.setOrientation(LinearLayout.HORIZONTAL);
|
||||
} else {
|
||||
overlay.setOrientation(LinearLayout.VERTICAL);
|
||||
}
|
||||
|
||||
scannerView.onResume();
|
||||
scannerView.setPreviewCallback(scanningThread);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,7 +97,6 @@ public final class FeatureFlags {
|
|||
private static final String STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum";
|
||||
private static final String GIFT_BADGE_RECEIVE_SUPPORT = "android.giftBadges.receiving";
|
||||
private static final String GIFT_BADGE_SEND_SUPPORT = "android.giftBadges.sending";
|
||||
private static final String USE_QR_LEGACY_SCAN = "android.qr.legacy_scan";
|
||||
private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";
|
||||
private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
|
||||
|
||||
|
@ -150,7 +149,6 @@ public final class FeatureFlags {
|
|||
STORIES_AUTO_DOWNLOAD_MAXIMUM,
|
||||
GIFT_BADGE_RECEIVE_SUPPORT,
|
||||
GIFT_BADGE_SEND_SUPPORT,
|
||||
USE_QR_LEGACY_SCAN,
|
||||
TELECOM_MANUFACTURER_ALLOWLIST,
|
||||
TELECOM_MODEL_BLOCKLIST
|
||||
);
|
||||
|
@ -212,7 +210,6 @@ public final class FeatureFlags {
|
|||
USE_AEC3,
|
||||
PAYMENTS_COUNTRY_BLOCKLIST,
|
||||
USE_FCM_FOREGROUND_SERVICE,
|
||||
USE_QR_LEGACY_SCAN,
|
||||
TELECOM_MANUFACTURER_ALLOWLIST,
|
||||
TELECOM_MODEL_BLOCKLIST
|
||||
);
|
||||
|
@ -530,10 +527,6 @@ public final class FeatureFlags {
|
|||
return giftBadgeReceiveSupport() && getBoolean(GIFT_BADGE_SEND_SUPPORT, Environment.IS_STAGING);
|
||||
}
|
||||
|
||||
public static boolean useQrLegacyScan() {
|
||||
return getBoolean(USE_QR_LEGACY_SCAN, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package org.thoughtcrime.securesms.verify
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -9,28 +7,24 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import androidx.core.view.OneShotPreDrawListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.qr.QrScannerView
|
||||
import org.signal.qr.kitkat.ScanListener
|
||||
import org.signal.qr.kitkat.ScanningThread
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ShapeScrim
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
* QR Scanner for identity verification
|
||||
*/
|
||||
class VerifyScanFragment : Fragment() {
|
||||
private lateinit var cameraView: CameraView
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var cameraView: QrScannerView
|
||||
private lateinit var cameraScrim: ShapeScrim
|
||||
private lateinit var cameraMarks: ImageView
|
||||
private lateinit var scanningThread: ScanningThread
|
||||
private lateinit var scanListener: ScanListener
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
scanListener = requireListener()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_scan_fragment)
|
||||
|
@ -45,28 +39,17 @@ class VerifyScanFragment : Fragment() {
|
|||
val height = cameraScrim.scrimHeight
|
||||
ViewUtil.updateLayoutParams(cameraMarks, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
scanningThread = ScanningThread()
|
||||
scanningThread.setScanListener(scanListener)
|
||||
scanningThread.setCharacterSet("ISO-8859-1")
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
scanningThread.start()
|
||||
}
|
||||
cameraView.start(viewLifecycleOwner)
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cameraView.onPause()
|
||||
scanningThread.stopScanning()
|
||||
}
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
||||
super.onConfigurationChanged(newConfiguration)
|
||||
cameraView.onPause()
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
lifecycleDisposable += cameraView
|
||||
.qrData
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData: String ->
|
||||
findListener<ScanListener>()?.onQrDataFound(qrData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.camera.CameraView
|
||||
android:id="@+id/scanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:camera="0" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:weightSum="2">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ShapeScrim
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:windowBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Position the QR code within the frame"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -23,11 +23,10 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/payments_transfer_scan_qr">
|
||||
|
||||
<org.thoughtcrime.securesms.components.camera.CameraView
|
||||
<org.signal.qr.QrScannerView
|
||||
android:id="@+id/scanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:camera="0" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/overlay"
|
||||
|
|
|
@ -15,11 +15,10 @@
|
|||
app:layout_constraintEnd_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.camera.CameraView
|
||||
<org.signal.qr.QrScannerView
|
||||
android:id="@+id/scanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:camera="0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -40,6 +40,7 @@ class QrScannerView @JvmOverloads constructor(
|
|||
this.scannerView = (scannerView as ScannerView)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun start(lifecycleOwner: LifecycleOwner, forceLegacy: Boolean = false) {
|
||||
if (scannerView != null) {
|
||||
Log.w(TAG, "Attempt to start scanning that has already started")
|
||||
|
|
Loading…
Add table
Reference in a new issue