diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt index 026e9e4e24..754898b150 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanFragment.kt @@ -1,35 +1,56 @@ package org.thoughtcrime.securesms.verify -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.lifecycle.viewmodel.compose.viewModel -import org.signal.camera.CameraScreenViewModel -import org.signal.core.ui.compose.ComposeFragment +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +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.core.util.concurrent.LifecycleDisposable +import org.signal.qr.QrScannerView import org.signal.qr.kitkat.ScanListener +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ShapeScrim +import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig +import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.fragments.findListener /** * QR Scanner for identity verification */ -class VerifyScanFragment : ComposeFragment() { - @Composable - override fun FragmentContent() { - val viewModel = viewModel { - CameraScreenViewModel() +class VerifyScanFragment : Fragment() { + private val lifecycleDisposable = LifecycleDisposable() + + private lateinit var cameraView: QrScannerView + private lateinit var cameraScrim: ShapeScrim + private lateinit var cameraMarks: ImageView + + override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { + return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_scan_fragment) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + cameraView = view.findViewById(R.id.scanner) + cameraScrim = view.findViewById(R.id.camera_scrim) + cameraMarks = view.findViewById(R.id.camera_marks) + OneShotPreDrawListener.add(cameraScrim) { + val width = cameraScrim.scrimWidth + val height = cameraScrim.scrimHeight + ViewUtil.updateLayoutParams(cameraMarks, width, height) } - val state by viewModel.state + cameraView.start(viewLifecycleOwner, CameraXRemoteConfig.isBlocklisted()) - LaunchedEffect(viewModel) { - viewModel.qrCodeDetected.collect { - findListener()?.onQrDataFound(it) + lifecycleDisposable.bindTo(viewLifecycleOwner) + + lifecycleDisposable += cameraView + .qrData + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { qrData: String -> + findListener()?.onQrDataFound(qrData) } - } - - VerifyScanScreen( - state = state, - emitter = viewModel::onEvent - ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanScreen.kt deleted file mode 100644 index a2bf703435..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyScanScreen.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.verify - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import org.signal.camera.CameraCaptureMode -import org.signal.camera.CameraScreen -import org.signal.camera.CameraScreenEvents -import org.signal.camera.CameraScreenState -import org.signal.camera.CameraScreenViewModel -import org.signal.core.ui.compose.Cutout -import org.signal.core.ui.compose.DayNightPreviews -import org.signal.core.ui.compose.Previews -import org.thoughtcrime.securesms.R - -/** - * Scanner screen for verifying user identities. This is meant to be utilized with an instance of - * [CameraScreenViewModel], which the parent component owns. There is a field on the ViewModel through - * which you can recieve via [qrCodeDetected] - */ -@Composable -fun VerifyScanScreen( - state: CameraScreenState, - emitter: (CameraScreenEvents) -> Unit -) { - Column { - CameraScreen( - state = state, - emitter = emitter, - roundCorners = false, - enableQrScanning = true, - captureMode = CameraCaptureMode.ImageOnly, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Cutout( - cutoutShape = RoundedCornerShape(18.dp), - cutoutPadding = PaddingValues(64.dp), - modifier = Modifier.fillMaxSize() - ) - - Image( - painter = painterResource(R.drawable.ic_camera_outline), - contentDescription = null, - modifier = Modifier.align(Alignment.Center) - ) - } - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.heightIn(min = 60.dp) - ) { - Text( - text = stringResource(R.string.verify_scan_fragment__scan_the_qr_code_on_your_contact), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth() - ) - } - } -} - -@DayNightPreviews -@Composable -private fun VerifyScanScreenPreview() { - Previews.Preview { - VerifyScanScreen( - state = CameraScreenState(), - emitter = {} - ) - } -} diff --git a/app/src/main/res/layout/verify_scan_fragment.xml b/app/src/main/res/layout/verify_scan_fragment.xml new file mode 100644 index 0000000000..f4f4f66620 --- /dev/null +++ b/app/src/main/res/layout/verify_scan_fragment.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/Cutout.kt b/core/ui/src/main/java/org/signal/core/ui/compose/Cutout.kt deleted file mode 100644 index 91c7085474..0000000000 --- a/core/ui/src/main/java/org/signal/core/ui/compose/Cutout.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2026 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.signal.core.ui.compose - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.ClipOp -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.addOutline -import androidx.compose.ui.graphics.drawscope.clipPath -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.signal.core.ui.compose.theme.SignalTheme -import kotlin.math.min - -@Composable -fun Cutout( - cutoutShape: Shape, - modifier: Modifier = Modifier, - cutoutPadding: PaddingValues = PaddingValues.Zero, - fillColor: Color = SignalTheme.colors.colorTransparent4 -) { - BoxWithConstraints( - modifier = modifier - ) { - val width = maxWidth - val height = maxHeight - - val density = LocalDensity.current - val layoutDirection = LocalLayoutDirection.current - - val pxPad: Rect = remember(cutoutPadding, density, layoutDirection) { - with(density) { - Rect( - left = cutoutPadding.calculateLeftPadding(layoutDirection).roundToPx().toFloat(), - top = cutoutPadding.calculateTopPadding().roundToPx().toFloat(), - right = cutoutPadding.calculateRightPadding(layoutDirection).roundToPx().toFloat(), - bottom = cutoutPadding.calculateBottomPadding().roundToPx().toFloat() - ) - } - } - - val pxSize = remember(density, width, height, pxPad) { - with(density) { - val size = min(width.roundToPx(), height.roundToPx()).toFloat() - - Size(size - pxPad.left - pxPad.right, size - pxPad.top - pxPad.bottom) - } - } - - val path = remember(density, layoutDirection, pxSize) { - val outline = cutoutShape.createOutline(pxSize, layoutDirection, density) - Path().apply { - addOutline(outline) - } - } - - val shapeOffset = remember(pxSize) { - Offset(pxSize.width / 2f, pxSize.height / 2f) - } - - val clipPath = remember { Path() } - - Canvas(modifier = Modifier.fillMaxSize()) { - clipPath.reset() - clipPath.addPath(path, center - shapeOffset) - - clipPath(clipPath, clipOp = ClipOp.Difference) { - drawRect(color = fillColor) - } - } - } -} - -@Preview -@Composable -fun CutoutPreview() { - Previews.Preview { - Box( - modifier = Modifier - .fillMaxSize() - .background( - brush = Brush.linearGradient( - colors = listOf(Color.Red, Color.Green) - ) - ) - ) - - Cutout( - cutoutShape = CircleShape, - cutoutPadding = PaddingValues(24.dp), - modifier = Modifier.fillMaxSize() - ) - } -}