mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-14 12:10:36 +01:00
Switch username scanner to use CameraScreen.
This commit is contained in:
+19
-24
@@ -7,7 +7,6 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -49,8 +48,7 @@ import androidx.core.app.ShareCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
@@ -58,8 +56,10 @@ import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.camera.CameraScreenViewModel
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
@@ -69,7 +69,6 @@ import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
@@ -85,7 +84,6 @@ import java.util.UUID
|
||||
class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var galleryLauncher: ActivityResultLauncher<Unit>
|
||||
|
||||
@@ -115,21 +113,29 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent
|
||||
val helpText = stringResource(id = R.string.UsernameLinkSettings_scan_this_qr_code)
|
||||
|
||||
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
|
||||
val cameraState by cameraViewModel.state
|
||||
|
||||
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) {
|
||||
viewModel.onTabSelected(ActiveTab.Scan)
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
cameraViewModel.qrCodeDetected.collect { data ->
|
||||
viewModel.onQrCodeScanned(data)
|
||||
}
|
||||
}
|
||||
|
||||
MainScreen(
|
||||
state = state,
|
||||
navController = navController,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
disposables = disposables.disposables,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraViewModel::onEvent,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
|
||||
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
|
||||
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
|
||||
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
|
||||
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
|
||||
onQrResultHandled = { viewModel.onQrResultHandled() },
|
||||
onOpenCameraClicked = { askCameraPermissions() },
|
||||
onOpenGalleryClicked = { galleryLauncher.launch(Unit) },
|
||||
@@ -143,10 +149,6 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onResume()
|
||||
@@ -167,14 +169,13 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
private fun MainScreen(
|
||||
state: UsernameLinkSettingsState,
|
||||
navController: NavController? = null,
|
||||
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
|
||||
disposables: CompositeDisposable = CompositeDisposable(),
|
||||
cameraState: CameraScreenState = CameraScreenState(),
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit = {},
|
||||
cameraPermissionState: PermissionState = previewPermissionState(),
|
||||
onCodeTabSelected: () -> Unit = {},
|
||||
onScanTabSelected: () -> Unit = {},
|
||||
onUsernameLinkResetResultHandled: () -> Unit = {},
|
||||
onShareBadge: () -> Unit = {},
|
||||
onQrCodeScanned: (String) -> Unit = {},
|
||||
onQrResultHandled: () -> Unit = {},
|
||||
onOpenCameraClicked: () -> Unit = {},
|
||||
onOpenGalleryClicked: () -> Unit = {},
|
||||
@@ -238,10 +239,9 @@ private fun MainScreen(
|
||||
exit = slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth })
|
||||
) {
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = disposables,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrCodeScanned,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraEmitter,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenCameraClicked = onOpenCameraClicked,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
@@ -394,11 +394,6 @@ private fun previewPermissionState(): PermissionState {
|
||||
}
|
||||
}
|
||||
|
||||
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
|
||||
override val lifecycle: Lifecycle
|
||||
get() = throw UnsupportedOperationException("Only for tests")
|
||||
}
|
||||
|
||||
private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
|
||||
if (badge == null) {
|
||||
return
|
||||
|
||||
+87
-25
@@ -1,45 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
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 androidx.lifecycle.LifecycleOwner
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.camera.CameraCaptureMode
|
||||
import org.signal.camera.CameraScreen
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.qr.QrScannerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
||||
import org.thoughtcrime.securesms.qr.QrScanScreens
|
||||
import org.thoughtcrime.securesms.qr.QrCrosshair
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A screen that allows you to scan a QR code to start a chat.
|
||||
*/
|
||||
@Composable
|
||||
fun UsernameQrScanScreen(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
disposables: CompositeDisposable,
|
||||
qrScanResult: QrScanResult?,
|
||||
onQrCodeScanned: (String) -> Unit,
|
||||
cameraState: CameraScreenState,
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenCameraClicked: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
@@ -88,26 +93,49 @@ fun UsernameQrScanScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, true)
|
||||
.background(Color.Black)
|
||||
) {
|
||||
QrScanScreens.QrScanScreen(
|
||||
factory = { context ->
|
||||
val view = QrScannerView(context)
|
||||
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
|
||||
onQrCodeScanned(data)
|
||||
if (hasCameraPermission) {
|
||||
CameraScreen(
|
||||
state = cameraState,
|
||||
emitter = cameraEmitter,
|
||||
enableQrScanning = true,
|
||||
captureMode = CameraCaptureMode.ImageOnly,
|
||||
roundCorners = false,
|
||||
fillViewport = true,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
QrCrosshair(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(48.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color.White
|
||||
)
|
||||
Buttons.MediumTonal(
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
onClick = onOpenCameraClicked
|
||||
) {
|
||||
Text(stringResource(R.string.CameraXFragment_allow_access))
|
||||
}
|
||||
view
|
||||
},
|
||||
update = { view ->
|
||||
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXRemoteConfig.isBlocklisted())
|
||||
},
|
||||
hasPermission = hasCameraPermission,
|
||||
onRequestPermissions = onOpenCameraClicked,
|
||||
qrHeaderLabelString = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
shape = CircleShape,
|
||||
containerColor = SignalTheme.colors.colorSurface1,
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 24.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 24.dp),
|
||||
onClick = onOpenGalleryClicked
|
||||
) {
|
||||
Image(
|
||||
@@ -143,3 +171,37 @@ private fun QrScanResultDialog(title: String? = null, message: String, onDismiss
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UsernameQrScanScreenPreview() {
|
||||
Previews.Preview {
|
||||
UsernameQrScanScreen(
|
||||
qrScanResult = null,
|
||||
cameraState = CameraScreenState(),
|
||||
cameraEmitter = {},
|
||||
onQrResultHandled = {},
|
||||
onOpenCameraClicked = {},
|
||||
onOpenGalleryClicked = {},
|
||||
onRecipientFound = {},
|
||||
hasCameraPermission = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UsernameQrScanScreenNoPermissionPreview() {
|
||||
Previews.Preview {
|
||||
UsernameQrScanScreen(
|
||||
qrScanResult = null,
|
||||
cameraState = CameraScreenState(),
|
||||
cameraEmitter = {},
|
||||
onQrResultHandled = {},
|
||||
onOpenCameraClicked = {},
|
||||
onOpenGalleryClicked = {},
|
||||
onRecipientFound = {},
|
||||
hasCameraPermission = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-14
@@ -24,22 +24,24 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.signal.camera.CameraScreenEvents
|
||||
import org.signal.camera.CameraScreenState
|
||||
import org.signal.camera.CameraScreenViewModel
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.signal.core.util.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -57,7 +59,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private val viewModel: UsernameQrScannerViewModel by viewModels()
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
@@ -66,7 +67,6 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
disposables.bindTo(this)
|
||||
|
||||
val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
|
||||
if (uri != null) {
|
||||
@@ -86,14 +86,22 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
val state by viewModel.state
|
||||
|
||||
val cameraViewModel: CameraScreenViewModel = viewModel { CameraScreenViewModel() }
|
||||
val cameraState by cameraViewModel.state
|
||||
|
||||
LaunchedEffect(cameraViewModel) {
|
||||
cameraViewModel.qrCodeDetected.collect { url ->
|
||||
viewModel.onQrScanned(url)
|
||||
}
|
||||
}
|
||||
|
||||
SignalTheme {
|
||||
Content(
|
||||
lifecycleOwner = this,
|
||||
diposables = disposables.disposables,
|
||||
state = state,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraViewModel::onEvent,
|
||||
galleryPermissionsState = galleryPermissionState,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
onQrScanned = { url -> viewModel.onQrScanned(url) },
|
||||
onQrResultHandled = {
|
||||
finish()
|
||||
},
|
||||
@@ -143,12 +151,11 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Content(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
diposables: CompositeDisposable,
|
||||
state: UsernameQrScannerViewModel.ScannerState,
|
||||
cameraState: CameraScreenState,
|
||||
cameraEmitter: (CameraScreenEvents) -> Unit,
|
||||
galleryPermissionsState: MultiplePermissionsState,
|
||||
cameraPermissionState: PermissionState,
|
||||
onQrScanned: (String) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenCameraClicked: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
@@ -173,10 +180,9 @@ fun Content(
|
||||
}
|
||||
) { contentPadding ->
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = diposables,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrScanned,
|
||||
cameraState = cameraState,
|
||||
cameraEmitter = cameraEmitter,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenCameraClicked = onOpenCameraClicked,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
|
||||
@@ -16,16 +16,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -41,6 +34,7 @@ import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.LinkDeviceResult
|
||||
import org.thoughtcrime.securesms.qr.QrCrosshair
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
@@ -115,8 +109,6 @@ fun LinkDeviceQrScanScreen(
|
||||
.background(Color.Black)
|
||||
) {
|
||||
if (hasPermission) {
|
||||
val crosshairPath = remember { Path() }
|
||||
|
||||
CameraScreen(
|
||||
state = cameraState,
|
||||
emitter = cameraEmitter,
|
||||
@@ -126,14 +118,7 @@ fun LinkDeviceQrScanScreen(
|
||||
fillViewport = true,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawQrCrosshair(crosshairPath)
|
||||
}
|
||||
)
|
||||
QrCrosshair(modifier = Modifier.fillMaxSize())
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.AddLinkDeviceFragment__scan_the_qr_code),
|
||||
@@ -173,43 +158,6 @@ fun LinkDeviceQrScanScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawQrCrosshair(path: Path) {
|
||||
val crosshairWidth: Float = size.minDimension * 0.6f
|
||||
val crosshairLineLength = crosshairWidth * 0.125f
|
||||
|
||||
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
|
||||
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
|
||||
|
||||
path.reset()
|
||||
|
||||
drawPath(
|
||||
path = path.apply {
|
||||
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
|
||||
lineTo(topLeft.x, topLeft.y)
|
||||
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
|
||||
|
||||
moveTo(topRight.x - crosshairLineLength, topRight.y)
|
||||
lineTo(topRight.x, topRight.y)
|
||||
lineTo(topRight.x, topRight.y + crosshairLineLength)
|
||||
|
||||
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
|
||||
lineTo(bottomRight.x, bottomRight.y)
|
||||
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
|
||||
|
||||
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
|
||||
},
|
||||
color = Color.White,
|
||||
style = Stroke(
|
||||
width = 3.dp.toPx(),
|
||||
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeToast(context: Context, messageId: Int, onLinkDeviceFailure: () -> Unit) {
|
||||
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show()
|
||||
onLinkDeviceFailure()
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.qr
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Four-corner crosshair overlay used to frame the QR scan target in camera viewfinders.
|
||||
* The crosshair is sized to 60% of the smaller dimension of its layout.
|
||||
*/
|
||||
@Composable
|
||||
fun QrCrosshair(modifier: Modifier = Modifier) {
|
||||
val path = remember { Path() }
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val crosshairWidth = size.minDimension * 0.6f
|
||||
val crosshairLineLength = crosshairWidth * 0.125f
|
||||
|
||||
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
|
||||
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
|
||||
|
||||
path.reset()
|
||||
|
||||
drawPath(
|
||||
path = path.apply {
|
||||
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
|
||||
lineTo(topLeft.x, topLeft.y)
|
||||
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
|
||||
|
||||
moveTo(topRight.x - crosshairLineLength, topRight.y)
|
||||
lineTo(topRight.x, topRight.y)
|
||||
lineTo(topRight.x, topRight.y + crosshairLineLength)
|
||||
|
||||
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
|
||||
lineTo(bottomRight.x, bottomRight.y)
|
||||
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
|
||||
|
||||
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
|
||||
},
|
||||
color = Color.White,
|
||||
style = Stroke(
|
||||
width = 3.dp.toPx(),
|
||||
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package org.thoughtcrime.securesms.qr
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.viewinterop.NoOpUpdate
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.qr.QrScannerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
object QrScanScreens {
|
||||
/**
|
||||
* Full-screen qr scanning screen with permission-asking UI
|
||||
*/
|
||||
@Composable
|
||||
fun QrScanScreen(
|
||||
factory: (Context) -> QrScannerView,
|
||||
update: (QrScannerView) -> Unit = NoOpUpdate,
|
||||
hasPermission: Boolean,
|
||||
onRequestPermissions: () -> Unit = {},
|
||||
qrHeaderLabelString: String
|
||||
) {
|
||||
val path = remember { Path() }
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, true)
|
||||
) {
|
||||
AndroidView(
|
||||
factory = factory,
|
||||
update = update,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (hasPermission) {
|
||||
drawQrCrosshair(path)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!hasPermission) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.align(Alignment.Center).padding(48.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color.White
|
||||
)
|
||||
Buttons.MediumTonal(
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
onClick = onRequestPermissions
|
||||
) {
|
||||
Text(stringResource(R.string.CameraXFragment_allow_access))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = qrHeaderLabelString,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 24.dp).fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawQrCrosshair(path: Path) {
|
||||
val crosshairWidth: Float = size.minDimension * 0.6f
|
||||
val crosshairLineLength = crosshairWidth * 0.125f
|
||||
|
||||
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
|
||||
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
|
||||
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
|
||||
|
||||
path.reset()
|
||||
|
||||
drawPath(
|
||||
path = path.apply {
|
||||
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
|
||||
lineTo(topLeft.x, topLeft.y)
|
||||
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
|
||||
|
||||
moveTo(topRight.x - crosshairLineLength, topRight.y)
|
||||
lineTo(topRight.x, topRight.y)
|
||||
lineTo(topRight.x, topRight.y + crosshairLineLength)
|
||||
|
||||
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
|
||||
lineTo(bottomRight.x, bottomRight.y)
|
||||
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
|
||||
|
||||
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y)
|
||||
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
|
||||
},
|
||||
color = Color.White,
|
||||
style = Stroke(
|
||||
width = 3.dp.toPx(),
|
||||
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user