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) {