Fix QR processing resolution and allow front camera use for device linking.

devel
Cody Henthorne 2022-10-13 09:57:44 -04:00 zatwierdzone przez Evan Perry Grove
rodzic 9e4449924d
commit c9cb8c3cd7
11 zmienionych plików z 215 dodań i 52 usunięć

Wyświetl plik

@ -10,6 +10,8 @@ import android.os.Bundle;
import android.os.Vibrator; import android.os.Vibrator;
import android.text.TextUtils; import android.text.TextUtils;
import android.transition.TransitionInflater; import android.transition.TransitionInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@ -54,6 +56,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
private DeviceAddFragment deviceAddFragment; private DeviceAddFragment deviceAddFragment;
private DeviceListFragment deviceListFragment; private DeviceListFragment deviceListFragment;
private DeviceLinkFragment deviceLinkFragment; private DeviceLinkFragment deviceLinkFragment;
private MenuItem cameraSwitchItem = null;
@Override @Override
public void onPreCreate() { public void onPreCreate() {
@ -102,6 +105,18 @@ public class DeviceActivity extends PassphraseRequiredActivity
return false; 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 @Override
public void onClick(View v) { public void onClick(View v) {
Permissions.with(this) Permissions.with(this)

Wyświetl plik

@ -2,24 +2,23 @@ package org.thoughtcrime.securesms;
import android.animation.Animator; import android.animation.Animator;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.res.Configuration;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewAnimationUtils; import android.view.ViewAnimationUtils;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import org.signal.qr.QrScannerView; import org.signal.qr.QrScannerView;
import org.signal.qr.kitkat.ScanListener; import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist; import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@ -32,12 +31,13 @@ public class DeviceAddFragment extends LoggingFragment {
private ImageView devicesImage; private ImageView devicesImage;
private ScanListener scanListener; private ScanListener scanListener;
private QrScannerView scannerView;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); 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); this.devicesImage = container.findViewById(R.id.devices);
ViewCompat.setTransitionName(devicesImage, "devices"); ViewCompat.setTransitionName(devicesImage, "devices");
@ -77,6 +77,19 @@ public class DeviceAddFragment extends LoggingFragment {
return container; 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() { public ImageView getDevicesImage() {
return devicesImage; return devicesImage;
} }

Wyświetl plik

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/device_add_camera_switch"
android:icon="@drawable/ic_switch_camera_24"
android:title="@string/CameraXFragment_change_camera_description"
app:iconTint="@color/signal_icon_tint_primary"
app:showAsAction="always"/>
</menu>

Wyświetl plik

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.ThreadUtil 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.ImageProxyLuminanceSource
import org.signal.qr.QrProcessor import org.signal.qr.QrProcessor
import org.signal.qr.QrScannerView import org.signal.qr.QrScannerView
class QrMainActivity : AppCompatActivity() { class QrMainActivity : AppCompatActivity() {
private lateinit var text: EditText
@SuppressLint("NewApi", "SetTextI18n") @SuppressLint("NewApi", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { 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) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
text = findViewById(R.id.log)
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1) requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1)
} }
@ -56,5 +105,9 @@ class QrMainActivity : AppCompatActivity() {
} }
} }
} }
findViewById<View>(R.id.camera_switch).setOnClickListener {
scanner.toggleCamera()
}
} }
} }

Wyświetl plik

@ -1,35 +1,57 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout <Button
android:layout_width="match_parent" android:id="@+id/camera_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:text="Change Camera" />
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/text_qr_data" android:id="@+id/text_qr_data"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content" />
<TextView <TextView
android:id="@+id/text_size" android:id="@+id/text_size"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/teal_200">
<org.signal.qr.QrScannerView <org.signal.qr.QrScannerView
android:id="@+id/scanner" android:id="@+id/scanner"
android:layout_width="240dp" android:layout_width="240dp"
android:layout_height="240dp" /> android:layout_height="240dp" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/teal_700">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/scanner_source" android:id="@+id/scanner_source"
android:layout_width="240dp" android:layout_width="240dp"
android:layout_height="240dp" /> android:layout_height="240dp"
android:scaleType="fitCenter"
android:rotation="90"
tools:src="@tools:sample/backgrounds/scenic" />
</FrameLayout>
</LinearLayout> <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/log"
android:layout_width="match_parent"
android:layout_height="100dp" />
</FrameLayout> </LinearLayout>

Wyświetl plik

@ -67,7 +67,7 @@ class QrProcessor {
companion object { companion object {
private val TAG = Log.tag(QrProcessor::class.java) private val TAG = Log.tag(QrProcessor::class.java)
/** For debugging only */
var listener: ((LuminanceSource) -> Unit)? = null var listener: ((LuminanceSource) -> Unit)? = null
} }
} }

Wyświetl plik

@ -57,6 +57,11 @@ class QrScannerView @JvmOverloads constructor(
}) })
} }
fun toggleCamera() {
Log.d(TAG, "Toggling camera")
scannerView?.toggleCamera()
}
companion object { companion object {
private val TAG = Log.tag(QrScannerView::class.java) private val TAG = Log.tag(QrScannerView::class.java)
} }

Wyświetl plik

@ -7,4 +7,5 @@ import androidx.lifecycle.LifecycleOwner
*/ */
interface ScannerView { interface ScannerView {
fun start(lifecycleOwner: LifecycleOwner) fun start(lifecycleOwner: LifecycleOwner)
fun toggleCamera()
} }

Wyświetl plik

@ -18,8 +18,27 @@ internal class ScannerView19 constructor(
private val scanListener: ScanListener private val scanListener: ScanListener
) : FrameLayout(context), ScannerView { ) : FrameLayout(context), ScannerView {
private var lifecycleOwner: LifecycleOwner? = null
private var scanningThread: ScanningThread? = 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 { init {
cameraView = QrCameraView(context) cameraView = QrCameraView(context)
@ -28,22 +47,16 @@ internal class ScannerView19 constructor(
} }
override fun start(lifecycleOwner: LifecycleOwner) { override fun start(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { this.lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
override fun onResume(owner: LifecycleOwner) { this.lifecycleOwner = lifecycleOwner
val scanningThread = ScanningThread() lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
scanningThread.setScanListener(scanListener) }
cameraView.onResume()
cameraView.setPreviewCallback(scanningThread)
scanningThread.start()
this@ScannerView19.scanningThread = scanningThread override fun toggleCamera() {
} cameraView.toggleCamera()
lifecycleOwner?.let {
override fun onPause(owner: LifecycleOwner) { lifecycleObserver.onPause(it)
cameraView.onPause() lifecycleObserver.onResume(it)
scanningThread?.stopScanning() }
scanningThread = null
}
})
} }
} }

Wyświetl plik

@ -2,9 +2,9 @@ package org.signal.qr
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Size
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
@ -28,19 +28,42 @@ internal class ScannerView21 constructor(
private val listener: ScanListener private val listener: ScanListener
) : FrameLayout(context), ScannerView { ) : FrameLayout(context), ScannerView {
private var lifecyleOwner: LifecycleOwner? = null
private val analyzerExecutor = Executors.newSingleThreadExecutor() private val analyzerExecutor = Executors.newSingleThreadExecutor()
private var cameraProvider: ProcessCameraProvider? = null private var cameraProvider: ProcessCameraProvider? = null
private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var camera: Camera? = null private var camera: Camera? = null
private var previewView: PreviewView private var previewView: PreviewView
private val qrProcessor = QrProcessor() private val qrProcessor = QrProcessor()
private val lifecycleObserver: DefaultLifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
cameraProvider = null
camera = null
analyzerExecutor.shutdown()
}
}
init { init {
previewView = PreviewView(context) previewView = PreviewView(context)
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(previewView) 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) { override fun start(lifecycleOwner: LifecycleOwner) {
this.lifecyleOwner?.lifecycle?.removeObserver(lifecycleObserver)
this.lifecyleOwner = lifecycleOwner
previewView.post { previewView.post {
Log.i(TAG, "Starting") Log.i(TAG, "Starting")
ProcessCameraProvider.getInstance(context).apply { ProcessCameraProvider.getInstance(context).apply {
@ -54,13 +77,7 @@ internal class ScannerView21 constructor(
} }
} }
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
override fun onDestroy(owner: LifecycleOwner) {
cameraProvider = null
camera = null
analyzerExecutor.shutdown()
}
})
} }
private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) { private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) {
@ -71,10 +88,14 @@ internal class ScannerView21 constructor(
Log.i(TAG, "Initializing use cases") 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() val imageAnalysis = ImageAnalysis.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3) .setTargetResolution(resolution)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
@ -88,10 +109,13 @@ internal class ScannerView21 constructor(
} }
cameraProvider.unbindAll() cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(lifecycle, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis) camera = cameraProvider.bindToLifecycle(lifecycle, cameraSelector, preview, imageAnalysis)
preview.setSurfaceProvider(previewView.surfaceProvider) preview.setSurfaceProvider(previewView.surfaceProvider)
Log.d(TAG, "Preview: ${preview.resolutionInfo}")
Log.d(TAG, "Analysis: ${imageAnalysis.resolutionInfo}")
this.cameraProvider = cameraProvider this.cameraProvider = cameraProvider
} }

Wyświetl plik

@ -47,7 +47,7 @@ public class QrCameraView extends ViewGroup {
private final OnOrientationChange onOrientationChange; private final OnOrientationChange onOrientationChange;
private volatile Optional<Camera> camera = Optional.empty(); private volatile Optional<Camera> 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 volatile int displayOrientation = -1;
private @NonNull State state = State.PAUSED; private @NonNull State state = State.PAUSED;
@ -161,6 +161,14 @@ public class QrCameraView extends ViewGroup {
return state != State.PAUSED; 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") @SuppressWarnings("SuspiciousNameCombination")
@Override @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) { protected void onLayout(boolean changed, int l, int t, int r, int b) {