mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
Common camera UI (#6027)
This commit is contained in:
parent
433b3b6fb0
commit
e7365b355f
@ -235,8 +235,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(libs.androidx.biometrics)
|
implementation(libs.androidx.biometrics)
|
||||||
implementation(libs.androidx.camera.camera2)
|
implementation(libs.androidx.camera.camera2)
|
||||||
implementation(libs.androidx.camera.lifecycle)
|
|
||||||
implementation(libs.androidx.camera.view)
|
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.animation)
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
|||||||
@ -1,23 +1,13 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import androidx.camera.core.CameraSelector
|
|
||||||
import androidx.camera.core.ImageAnalysis
|
|
||||||
import androidx.camera.core.Preview
|
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
||||||
import androidx.camera.view.PreviewView
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@ -26,34 +16,23 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||||
import androidx.compose.ui.semantics.customActions
|
import androidx.compose.ui.semantics.customActions
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
import com.bitwarden.ui.platform.base.util.StatusBarsAppearanceAffect
|
import com.bitwarden.ui.platform.base.util.StatusBarsAppearanceAffect
|
||||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
|
import com.bitwarden.ui.platform.components.camera.CameraPreview
|
||||||
|
import com.bitwarden.ui.platform.components.camera.QrCodeSquare
|
||||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
|
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
|
||||||
@ -65,9 +44,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
|||||||
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
|
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
|
||||||
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme
|
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme
|
||||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen to scan QR codes for the application.
|
* The screen to scan QR codes for the application.
|
||||||
@ -221,184 +197,6 @@ private fun QrCodeContentMedium(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod", "TooGenericExceptionCaught")
|
|
||||||
@Composable
|
|
||||||
private fun CameraPreview(
|
|
||||||
cameraErrorReceive: () -> Unit,
|
|
||||||
qrCodeAnalyzer: QrCodeAnalyzer,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
var cameraProvider: ProcessCameraProvider? by remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
val previewView = remember {
|
|
||||||
PreviewView(context).apply {
|
|
||||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
MATCH_PARENT,
|
|
||||||
MATCH_PARENT,
|
|
||||||
)
|
|
||||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageAnalyzer = remember(qrCodeAnalyzer) {
|
|
||||||
ImageAnalysis.Builder()
|
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
||||||
.build()
|
|
||||||
.apply {
|
|
||||||
setAnalyzer(
|
|
||||||
Executors.newSingleThreadExecutor(),
|
|
||||||
qrCodeAnalyzer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val preview = Preview.Builder()
|
|
||||||
.build()
|
|
||||||
.apply { surfaceProvider = previewView.surfaceProvider }
|
|
||||||
|
|
||||||
// Unbind from the camera provider when we leave the screen.
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
cameraProvider?.unbindAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the camera provider on a background thread. This is necessary because
|
|
||||||
// ProcessCameraProvider.getInstance returns a ListenableFuture. For an example see
|
|
||||||
// https://github.com/JetBrains/compose-multiplatform/blob/1c7154b975b79901f40f28278895183e476ed36d/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt#L85
|
|
||||||
LaunchedEffect(imageAnalyzer) {
|
|
||||||
try {
|
|
||||||
cameraProvider = suspendCoroutine { continuation ->
|
|
||||||
ProcessCameraProvider.getInstance(context).also { future ->
|
|
||||||
future.addListener(
|
|
||||||
{ continuation.resume(future.get()) },
|
|
||||||
ContextCompat.getMainExecutor(context),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraProvider?.unbindAll()
|
|
||||||
if (cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) == true) {
|
|
||||||
cameraProvider?.bindToLifecycle(
|
|
||||||
lifecycleOwner,
|
|
||||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
|
||||||
preview,
|
|
||||||
imageAnalyzer,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
cameraErrorReceive()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
cameraErrorReceive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AndroidView(
|
|
||||||
factory = { previewView },
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI for the blue QR code square that is drawn onto the screen.
|
|
||||||
*/
|
|
||||||
@Suppress("MagicNumber", "LongMethod")
|
|
||||||
@Composable
|
|
||||||
private fun QrCodeSquare(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
squareOutlineSize: Dp,
|
|
||||||
) {
|
|
||||||
val color = BitwardenTheme.colorScheme.text.primary
|
|
||||||
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.size(squareOutlineSize),
|
|
||||||
) {
|
|
||||||
val strokeWidth = 3.dp.toPx()
|
|
||||||
|
|
||||||
val squareSize = size.width
|
|
||||||
val strokeOffset = strokeWidth / 2
|
|
||||||
val sideLength = (1f / 6) * squareSize
|
|
||||||
|
|
||||||
drawIntoCanvas { canvas ->
|
|
||||||
canvas.nativeCanvas.apply {
|
|
||||||
// Draw upper top left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(0f, strokeOffset),
|
|
||||||
end = Offset(sideLength, strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower top left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(strokeOffset, strokeOffset),
|
|
||||||
end = Offset(strokeOffset, sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper top right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - sideLength, strokeOffset),
|
|
||||||
end = Offset(squareSize - strokeOffset, strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower top right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, 0f),
|
|
||||||
end = Offset(squareSize - strokeOffset, sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper bottom right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, squareSize),
|
|
||||||
end = Offset(squareSize - strokeOffset, squareSize - sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower bottom right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, squareSize - strokeOffset),
|
|
||||||
end = Offset(squareSize - sideLength, squareSize - strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper bottom left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(strokeOffset, squareSize),
|
|
||||||
end = Offset(strokeOffset, squareSize - sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower bottom left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(0f, squareSize - strokeOffset),
|
|
||||||
end = Offset(sideLength, squareSize - strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EnterKeyManuallyText(
|
private fun EnterKeyManuallyText(
|
||||||
onEnterKeyManuallyClick: () -> Unit,
|
onEnterKeyManuallyClick: () -> Unit,
|
||||||
|
|||||||
@ -55,14 +55,6 @@ class QrCodeScanScreenTest : BitwardenComposeTest() {
|
|||||||
assertTrue(onNavigateToManualCodeEntryScreenCalled)
|
assertTrue(onNavigateToManualCodeEntryScreenCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when unable to setup camera CameraErrorReceive will be sent`() = runTest {
|
|
||||||
// Because the camera is not set up in the tests, this will always be triggered
|
|
||||||
verify {
|
|
||||||
viewModel.trySendAction(QrCodeScanAction.CameraSetupErrorReceive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when a scan is successful a result will be sent`() = runTest {
|
fun `when a scan is successful a result will be sent`() = runTest {
|
||||||
val result = "testCode"
|
val result = "testCode"
|
||||||
|
|||||||
@ -190,8 +190,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(libs.androidx.biometrics)
|
implementation(libs.androidx.biometrics)
|
||||||
implementation(libs.androidx.camera.camera2)
|
implementation(libs.androidx.camera.camera2)
|
||||||
implementation(libs.androidx.camera.lifecycle)
|
|
||||||
implementation(libs.androidx.camera.view)
|
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.animation)
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
|||||||
@ -1,23 +1,13 @@
|
|||||||
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
|
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import androidx.camera.core.CameraSelector
|
|
||||||
import androidx.camera.core.ImageAnalysis
|
|
||||||
import androidx.camera.core.Preview
|
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
||||||
import androidx.camera.view.PreviewView
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@ -25,30 +15,19 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
||||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
@ -57,6 +36,8 @@ import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
|||||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
|
import com.bitwarden.ui.platform.components.camera.CameraPreview
|
||||||
|
import com.bitwarden.ui.platform.components.camera.QrCodeSquare
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
|
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
|
||||||
@ -66,9 +47,6 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
|||||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
|
import com.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
|
||||||
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme
|
import com.bitwarden.ui.platform.theme.color.darkBitwardenColorScheme
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen to scan QR codes for the application.
|
* The screen to scan QR codes for the application.
|
||||||
@ -258,180 +236,6 @@ private fun LandscapeQRCodeContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod", "TooGenericExceptionCaught")
|
|
||||||
@Composable
|
|
||||||
private fun CameraPreview(
|
|
||||||
cameraErrorReceive: () -> Unit,
|
|
||||||
qrCodeAnalyzer: QrCodeAnalyzer,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
var cameraProvider: ProcessCameraProvider? by remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
val previewView = remember {
|
|
||||||
PreviewView(context).apply {
|
|
||||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
MATCH_PARENT,
|
|
||||||
MATCH_PARENT,
|
|
||||||
)
|
|
||||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageAnalyzer = remember(qrCodeAnalyzer) {
|
|
||||||
ImageAnalysis.Builder()
|
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
||||||
.build()
|
|
||||||
.apply {
|
|
||||||
setAnalyzer(
|
|
||||||
Executors.newSingleThreadExecutor(),
|
|
||||||
qrCodeAnalyzer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val preview = Preview.Builder()
|
|
||||||
.build()
|
|
||||||
.apply { surfaceProvider = previewView.surfaceProvider }
|
|
||||||
|
|
||||||
// Unbind from the camera provider when we leave the screen.
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
cameraProvider?.unbindAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the camera provider on a background thread. This is necessary because
|
|
||||||
// ProcessCameraProvider.getInstance returns a ListenableFuture. For an example see
|
|
||||||
// https://github.com/JetBrains/compose-multiplatform/blob/1c7154b975b79901f40f28278895183e476ed36d/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt#L85
|
|
||||||
LaunchedEffect(imageAnalyzer) {
|
|
||||||
try {
|
|
||||||
cameraProvider = suspendCoroutine { continuation ->
|
|
||||||
ProcessCameraProvider.getInstance(context).also { future ->
|
|
||||||
future.addListener(
|
|
||||||
{ continuation.resume(future.get()) },
|
|
||||||
Executors.newSingleThreadExecutor(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraProvider?.unbindAll()
|
|
||||||
cameraProvider?.bindToLifecycle(
|
|
||||||
lifecycleOwner,
|
|
||||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
|
||||||
preview,
|
|
||||||
imageAnalyzer,
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
cameraErrorReceive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AndroidView(
|
|
||||||
factory = { previewView },
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI for the blue QR code square that is drawn onto the screen.
|
|
||||||
*/
|
|
||||||
@Suppress("MagicNumber", "LongMethod")
|
|
||||||
@Composable
|
|
||||||
private fun QrCodeSquare(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
squareOutlineSize: Dp,
|
|
||||||
) {
|
|
||||||
val color = BitwardenTheme.colorScheme.text.primary
|
|
||||||
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(squareOutlineSize)
|
|
||||||
.padding(8.dp),
|
|
||||||
) {
|
|
||||||
val strokeWidth = 3.dp.toPx()
|
|
||||||
|
|
||||||
val squareSize = size.width
|
|
||||||
val strokeOffset = strokeWidth / 2
|
|
||||||
val sideLength = (1f / 6) * squareSize
|
|
||||||
|
|
||||||
drawIntoCanvas { canvas ->
|
|
||||||
canvas.nativeCanvas.apply {
|
|
||||||
// Draw upper top left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(0f, strokeOffset),
|
|
||||||
end = Offset(sideLength, strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower top left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(strokeOffset, strokeOffset),
|
|
||||||
end = Offset(strokeOffset, sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper top right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - sideLength, strokeOffset),
|
|
||||||
end = Offset(squareSize - strokeOffset, strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower top right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, 0f),
|
|
||||||
end = Offset(squareSize - strokeOffset, sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper bottom right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, squareSize),
|
|
||||||
end = Offset(squareSize - strokeOffset, squareSize - sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower bottom right.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(squareSize - strokeOffset, squareSize - strokeOffset),
|
|
||||||
end = Offset(squareSize - sideLength, squareSize - strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw upper bottom left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(strokeOffset, squareSize),
|
|
||||||
end = Offset(strokeOffset, squareSize - sideLength),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw lower bottom left.
|
|
||||||
drawLine(
|
|
||||||
color = color,
|
|
||||||
start = Offset(0f, squareSize - strokeOffset),
|
|
||||||
end = Offset(sideLength, squareSize - strokeOffset),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomClickableText(
|
private fun BottomClickableText(
|
||||||
onEnterCodeManuallyClick: () -> Unit,
|
onEnterCodeManuallyClick: () -> Unit,
|
||||||
|
|||||||
@ -58,6 +58,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(libs.androidx.camera.camera2)
|
implementation(libs.androidx.camera.camera2)
|
||||||
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
|
implementation(libs.androidx.camera.view)
|
||||||
implementation(libs.androidx.compose.animation)
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.compose.material3.adaptive)
|
implementation(libs.androidx.compose.material3.adaptive)
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
package com.bitwarden.ui.platform.components.camera
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable for displaying the camera preview.
|
||||||
|
*
|
||||||
|
* @param qrCodeAnalyzer The [QrCodeAnalyzer].
|
||||||
|
* @param cameraErrorReceive A callback invoked when an error occurs.
|
||||||
|
* @param modifier The [Modifier] for this composable.
|
||||||
|
* @param context The local context.
|
||||||
|
* @param lifecycleOwner The current lifecycle owner.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CameraPreview(
|
||||||
|
qrCodeAnalyzer: QrCodeAnalyzer,
|
||||||
|
cameraErrorReceive: (Exception) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
context: Context = LocalContext.current,
|
||||||
|
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
|
||||||
|
) {
|
||||||
|
var cameraProvider: ProcessCameraProvider? by remember { mutableStateOf(value = null) }
|
||||||
|
val previewView = remember {
|
||||||
|
PreviewView(context).apply {
|
||||||
|
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
|
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val imageAnalyzer = remember(qrCodeAnalyzer) {
|
||||||
|
ImageAnalysis.Builder()
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
.apply { setAnalyzer(Executors.newSingleThreadExecutor(), qrCodeAnalyzer) }
|
||||||
|
}
|
||||||
|
val preview = Preview.Builder()
|
||||||
|
.build()
|
||||||
|
.apply { surfaceProvider = previewView.surfaceProvider }
|
||||||
|
|
||||||
|
// Unbind from the camera provider when we leave the screen.
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
cameraProvider?.unbindAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the camera provider on a background thread. This is necessary because
|
||||||
|
// ProcessCameraProvider.getInstance returns a ListenableFuture. For an example see
|
||||||
|
// https://github.com/JetBrains/compose-multiplatform/blob/1c7154b975b79901f40f28278895183e476ed36d/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt#L85
|
||||||
|
LaunchedEffect(imageAnalyzer) {
|
||||||
|
try {
|
||||||
|
cameraProvider = suspendCoroutine { continuation ->
|
||||||
|
ProcessCameraProvider.getInstance(context).also { future ->
|
||||||
|
future.addListener(
|
||||||
|
{ continuation.resume(value = future.get()) },
|
||||||
|
ContextCompat.getMainExecutor(context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraProvider
|
||||||
|
?.let {
|
||||||
|
it.unbindAll()
|
||||||
|
if (it.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) {
|
||||||
|
it.bindToLifecycle(
|
||||||
|
lifecycleOwner = lifecycleOwner,
|
||||||
|
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
|
useCases = arrayOf(preview, imageAnalyzer),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cameraErrorReceive(IllegalStateException("Missing Back Camera"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: cameraErrorReceive(IllegalStateException("Missing Camera Provider"))
|
||||||
|
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||||
|
cameraErrorReceive(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { previewView },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
package com.bitwarden.ui.platform.components.camera
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UI for the QR code square overlay that is drawn onto the screen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun QrCodeSquare(
|
||||||
|
squareOutlineSize: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
strokeWidth: Dp = 3.dp,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
QrCodeSquareCanvas(
|
||||||
|
color = color,
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(size = squareOutlineSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber", "LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun QrCodeSquareCanvas(
|
||||||
|
color: Color,
|
||||||
|
strokeWidth: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
val strokeWidthPx = strokeWidth.toPx()
|
||||||
|
val squareSize = size.width
|
||||||
|
val strokeOffset = strokeWidthPx / 2
|
||||||
|
val sideLength = (1f / 6) * squareSize
|
||||||
|
drawIntoCanvas { canvas ->
|
||||||
|
canvas.nativeCanvas.apply {
|
||||||
|
// Draw upper top left.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = 0f, y = strokeOffset),
|
||||||
|
end = Offset(x = sideLength, y = strokeOffset),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw lower top left.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = strokeOffset, y = strokeOffset),
|
||||||
|
end = Offset(x = strokeOffset, y = sideLength),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw upper top right.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = squareSize - sideLength, y = strokeOffset),
|
||||||
|
end = Offset(x = squareSize - strokeOffset, y = strokeOffset),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw lower top right.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = squareSize - strokeOffset, y = 0f),
|
||||||
|
end = Offset(x = squareSize - strokeOffset, y = sideLength),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw upper bottom right.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = squareSize - strokeOffset, y = squareSize),
|
||||||
|
end = Offset(x = squareSize - strokeOffset, y = squareSize - sideLength),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw lower bottom right.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = squareSize - strokeOffset, y = squareSize - strokeOffset),
|
||||||
|
end = Offset(x = squareSize - sideLength, y = squareSize - strokeOffset),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw upper bottom left.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = strokeOffset, y = squareSize),
|
||||||
|
end = Offset(x = strokeOffset, y = squareSize - sideLength),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
// Draw lower bottom left.
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = 0f, y = squareSize - strokeOffset),
|
||||||
|
end = Offset(x = sideLength, y = squareSize - strokeOffset),
|
||||||
|
strokeWidth = strokeWidthPx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user