From ea9bf0ccd566c9b85e3216e19f86df08c46b3136 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 13 Oct 2022 09:57:44 -0400 Subject: [PATCH] Fix QR processing resolution and allow front camera use for device linking. --- .../securesms/DeviceActivity.java | 15 +++++ .../securesms/DeviceAddFragment.java | 21 +++++-- app/src/main/res/menu/device_add.xml | 9 +++ .../java/org/signal/qrtest/QrMainActivity.kt | 53 +++++++++++++++++ qr/app/src/main/res/layout/activity_main.xml | 58 +++++++++++++------ .../main/java/org/signal/qr/QrProcessor.kt | 2 +- .../main/java/org/signal/qr/QrScannerView.kt | 5 ++ .../main/java/org/signal/qr/ScannerView.kt | 1 + .../main/java/org/signal/qr/ScannerView19.kt | 47 +++++++++------ .../main/java/org/signal/qr/ScannerView21.kt | 46 +++++++++++---- .../org/signal/qr/kitkat/QrCameraView.java | 10 +++- 11 files changed, 215 insertions(+), 52 deletions(-) create mode 100644 app/src/main/res/menu/device_add.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index 67eb27b2d..4125fd9a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -10,6 +10,8 @@ import android.os.Bundle; import android.os.Vibrator; import android.text.TextUtils; import android.transition.TransitionInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Button; @@ -54,6 +56,7 @@ public class DeviceActivity extends PassphraseRequiredActivity private DeviceAddFragment deviceAddFragment; private DeviceListFragment deviceListFragment; private DeviceLinkFragment deviceLinkFragment; + private MenuItem cameraSwitchItem = null; @Override public void onPreCreate() { @@ -102,6 +105,18 @@ public class DeviceActivity extends PassphraseRequiredActivity return false; } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.device_add, menu); + cameraSwitchItem = menu.findItem(R.id.device_add_camera_switch); + cameraSwitchItem.setVisible(false); + return super.onCreateOptionsMenu(menu); + } + + public MenuItem getCameraSwitchItem() { + return cameraSwitchItem; + } + @Override public void onClick(View v) { Permissions.with(this) diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java index 1a60f1888..596511b0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceAddFragment.java @@ -2,24 +2,23 @@ package org.thoughtcrime.securesms; import android.animation.Animator; import android.annotation.TargetApi; -import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; -import android.widget.LinearLayout; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import org.signal.qr.QrScannerView; import org.signal.qr.kitkat.ScanListener; import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.ViewUtil; @@ -32,12 +31,13 @@ public class DeviceAddFragment extends LoggingFragment { private ImageView devicesImage; private ScanListener scanListener; + private QrScannerView scannerView; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); - QrScannerView scannerView = container.findViewById(R.id.scanner); + this.scannerView = container.findViewById(R.id.scanner); this.devicesImage = container.findViewById(R.id.devices); ViewCompat.setTransitionName(devicesImage, "devices"); @@ -77,6 +77,19 @@ public class DeviceAddFragment extends LoggingFragment { return container; } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + MenuItem switchCamera = ((DeviceActivity) requireActivity()).getCameraSwitchItem(); + + if (switchCamera != null) { + switchCamera.setVisible(true); + switchCamera.setOnMenuItemClickListener(v -> { + scannerView.toggleCamera(); + return true; + }); + } + } + public ImageView getDevicesImage() { return devicesImage; } diff --git a/app/src/main/res/menu/device_add.xml b/app/src/main/res/menu/device_add.xml new file mode 100644 index 000000000..14c8e2b93 --- /dev/null +++ b/app/src/main/res/menu/device_add.xml @@ -0,0 +1,9 @@ + + + + diff --git a/qr/app/src/main/java/org/signal/qrtest/QrMainActivity.kt b/qr/app/src/main/java/org/signal/qrtest/QrMainActivity.kt index ed8074ce7..37c0806c8 100644 --- a/qr/app/src/main/java/org/signal/qrtest/QrMainActivity.kt +++ b/qr/app/src/main/java/org/signal/qrtest/QrMainActivity.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Build import android.os.Bundle +import android.view.View +import android.widget.EditText import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity @@ -12,16 +14,63 @@ import com.google.zxing.PlanarYUVLuminanceSource import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.ThreadUtil +import org.signal.core.util.logging.AndroidLogger +import org.signal.core.util.logging.Log import org.signal.qr.ImageProxyLuminanceSource import org.signal.qr.QrProcessor import org.signal.qr.QrScannerView class QrMainActivity : AppCompatActivity() { + + + private lateinit var text: EditText + @SuppressLint("NewApi", "SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { + Log.initialize(AndroidLogger(), object : Log.Logger() { + override fun v(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) { + printlnFormatted('v', tag, message, t) + } + + override fun d(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) { + printlnFormatted('d', tag, message, t) + } + + override fun i(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) { + printlnFormatted('i', tag, message, t) + } + + override fun w(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) { + printlnFormatted('w', tag, message, t) + } + + override fun e(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) { + printlnFormatted('e', tag, message, t) + } + + override fun flush() {} + + private fun printlnFormatted(level: Char, tag: String, message: String?, t: Throwable?) { + ThreadUtil.runOnMain { + val allText = text.text.toString() + "\n" + format(level, tag, message, t) + text.setText(allText) + } + } + + private fun format(level: Char, tag: String, message: String?, t: Throwable?): String { + return if (t != null) { + String.format("%c[%s] %s %s:%s", level, tag, message, t.javaClass.simpleName, t.message) + } else { + String.format("%c[%s] %s", level, tag, message) + } + } + }) + super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + text = findViewById(R.id.log) + if (Build.VERSION.SDK_INT >= 23) { requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1) } @@ -56,5 +105,9 @@ class QrMainActivity : AppCompatActivity() { } } } + + findViewById(R.id.camera_switch).setOnClickListener { + scanner.toggleCamera() + } } } diff --git a/qr/app/src/main/res/layout/activity_main.xml b/qr/app/src/main/res/layout/activity_main.xml index 2e3d0d27e..1989a03c6 100644 --- a/qr/app/src/main/res/layout/activity_main.xml +++ b/qr/app/src/main/res/layout/activity_main.xml @@ -1,35 +1,57 @@ - + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center_horizontal" + android:orientation="vertical"> - + android:text="Change Camera" /> - + - + + + + + + + + android:layout_height="240dp" + android:scaleType="fitCenter" + android:rotation="90" + tools:src="@tools:sample/backgrounds/scenic" /> + - + - \ No newline at end of file + diff --git a/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt b/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt index 03f87a707..b4167448a 100644 --- a/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt +++ b/qr/lib/src/main/java/org/signal/qr/QrProcessor.kt @@ -67,7 +67,7 @@ class QrProcessor { companion object { private val TAG = Log.tag(QrProcessor::class.java) - + /** For debugging only */ var listener: ((LuminanceSource) -> Unit)? = null } } diff --git a/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt b/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt index 8f26f8a5f..a285aa8b6 100644 --- a/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt +++ b/qr/lib/src/main/java/org/signal/qr/QrScannerView.kt @@ -57,6 +57,11 @@ class QrScannerView @JvmOverloads constructor( }) } + fun toggleCamera() { + Log.d(TAG, "Toggling camera") + scannerView?.toggleCamera() + } + companion object { private val TAG = Log.tag(QrScannerView::class.java) } diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView.kt index 2e064edef..8a4774c69 100644 --- a/qr/lib/src/main/java/org/signal/qr/ScannerView.kt +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView.kt @@ -7,4 +7,5 @@ import androidx.lifecycle.LifecycleOwner */ interface ScannerView { fun start(lifecycleOwner: LifecycleOwner) + fun toggleCamera() } diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt index d27adf480..617f03c11 100644 --- a/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView19.kt @@ -18,8 +18,27 @@ internal class ScannerView19 constructor( private val scanListener: ScanListener ) : FrameLayout(context), ScannerView { + private var lifecycleOwner: LifecycleOwner? = null private var scanningThread: ScanningThread? = null - private val cameraView: QrCameraView + private lateinit var cameraView: QrCameraView + + private val lifecycleObserver = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + val scanningThread = ScanningThread() + scanningThread.setScanListener(scanListener) + cameraView.onResume() + cameraView.setPreviewCallback(scanningThread) + scanningThread.start() + + this@ScannerView19.scanningThread = scanningThread + } + + override fun onPause(owner: LifecycleOwner) { + cameraView.onPause() + scanningThread?.stopScanning() + scanningThread = null + } + } init { cameraView = QrCameraView(context) @@ -28,22 +47,16 @@ internal class ScannerView19 constructor( } override fun start(lifecycleOwner: LifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - val scanningThread = ScanningThread() - scanningThread.setScanListener(scanListener) - cameraView.onResume() - cameraView.setPreviewCallback(scanningThread) - scanningThread.start() + this.lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) + this.lifecycleOwner = lifecycleOwner + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + } - this@ScannerView19.scanningThread = scanningThread - } - - override fun onPause(owner: LifecycleOwner) { - cameraView.onPause() - scanningThread?.stopScanning() - scanningThread = null - } - }) + override fun toggleCamera() { + cameraView.toggleCamera() + lifecycleOwner?.let { + lifecycleObserver.onPause(it) + lifecycleObserver.onResume(it) + } } } diff --git a/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt b/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt index 1a34f622c..d3b083566 100644 --- a/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt +++ b/qr/lib/src/main/java/org/signal/qr/ScannerView21.kt @@ -2,9 +2,9 @@ package org.signal.qr import android.annotation.SuppressLint import android.content.Context +import android.util.Size import android.widget.FrameLayout import androidx.annotation.RequiresApi -import androidx.camera.core.AspectRatio import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis @@ -28,19 +28,42 @@ internal class ScannerView21 constructor( private val listener: ScanListener ) : FrameLayout(context), ScannerView { + private var lifecyleOwner: LifecycleOwner? = null private val analyzerExecutor = Executors.newSingleThreadExecutor() private var cameraProvider: ProcessCameraProvider? = null + private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA private var camera: Camera? = null private var previewView: PreviewView private val qrProcessor = QrProcessor() + private val lifecycleObserver: DefaultLifecycleObserver = object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + cameraProvider = null + camera = null + analyzerExecutor.shutdown() + } + } + init { previewView = PreviewView(context) previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(previewView) } + override fun toggleCamera() { + cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + + lifecyleOwner?.let { start(it) } + } + override fun start(lifecycleOwner: LifecycleOwner) { + this.lifecyleOwner?.lifecycle?.removeObserver(lifecycleObserver) + this.lifecyleOwner = lifecycleOwner + previewView.post { Log.i(TAG, "Starting") ProcessCameraProvider.getInstance(context).apply { @@ -54,13 +77,7 @@ internal class ScannerView21 constructor( } } - lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - cameraProvider = null - camera = null - analyzerExecutor.shutdown() - } - }) + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) } private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) { @@ -71,10 +88,14 @@ internal class ScannerView21 constructor( Log.i(TAG, "Initializing use cases") - val preview = Preview.Builder().build() + val resolution = Size(480, 640) + + val preview = Preview.Builder() + .setTargetResolution(resolution) + .build() val imageAnalysis = ImageAnalysis.Builder() - .setTargetAspectRatio(AspectRatio.RATIO_4_3) + .setTargetResolution(resolution) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() @@ -88,10 +109,13 @@ internal class ScannerView21 constructor( } cameraProvider.unbindAll() - camera = cameraProvider.bindToLifecycle(lifecycle, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis) + camera = cameraProvider.bindToLifecycle(lifecycle, cameraSelector, preview, imageAnalysis) preview.setSurfaceProvider(previewView.surfaceProvider) + Log.d(TAG, "Preview: ${preview.resolutionInfo}") + Log.d(TAG, "Analysis: ${imageAnalysis.resolutionInfo}") + this.cameraProvider = cameraProvider } diff --git a/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java b/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java index 3172d77a4..9047acab7 100644 --- a/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java +++ b/qr/lib/src/main/java/org/signal/qr/kitkat/QrCameraView.java @@ -47,7 +47,7 @@ public class QrCameraView extends ViewGroup { private final OnOrientationChange onOrientationChange; private volatile Optional camera = Optional.empty(); - private final int cameraId = CameraInfo.CAMERA_FACING_BACK; + private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK; private volatile int displayOrientation = -1; private @NonNull State state = State.PAUSED; @@ -161,6 +161,14 @@ public class QrCameraView extends ViewGroup { return state != State.PAUSED; } + public void toggleCamera() { + if (cameraId == CameraInfo.CAMERA_FACING_BACK) { + cameraId = CameraInfo.CAMERA_FACING_FRONT; + } else { + cameraId = CameraInfo.CAMERA_FACING_BACK; + } + } + @SuppressWarnings("SuspiciousNameCombination") @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {