Migrate all QR scanning to new scanner.

This commit is contained in:
Cody Henthorne 2022-08-03 11:32:53 -04:00 committed by Greyson Parrelli
parent caf1329005
commit 18eac51576
11 changed files with 46 additions and 852 deletions

View file

@ -57,7 +57,7 @@ public class DeviceAddFragment extends LoggingFragment {
});
}
scannerView.start(getViewLifecycleOwner(), FeatureFlags.useQrLegacyScan());
scannerView.start(getViewLifecycleOwner());
lifecycleDisposable.bindTo(getViewLifecycleOwner());

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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)
}
}
}

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -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")