PM-26594: Move the QrCodeAnalyzer to the UI module (#5980)

This commit is contained in:
David Perez 2025-10-07 12:04:16 -05:00 committed by GitHub
parent cd9c7f98e7
commit 7849bbbb0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 19 additions and 126 deletions

View File

@ -271,7 +271,6 @@ dependencies {
implementation(platform(libs.square.retrofit.bom)) implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit) implementation(libs.square.retrofit)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only // For now we are restricted to running Compose tests for debug builds only
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@ -55,6 +55,8 @@ 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.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.QrCodeAnalyzerImpl
import com.bitwarden.ui.platform.model.WindowSize import com.bitwarden.ui.platform.model.WindowSize
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
@ -62,8 +64,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 com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util.QrCodeAnalyzer
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util.QrCodeAnalyzerImpl
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine

View File

@ -1,73 +0,0 @@
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util
import androidx.camera.core.ImageProxy
import com.bitwarden.annotation.OmitFromCoverage
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.common.HybridBinarizer
import java.nio.ByteBuffer
/**
* A class setup to handle image analysis so that we can use the Zxing library
* to scan QR codes and convert them to a string.
*/
@OmitFromCoverage
class QrCodeAnalyzerImpl : QrCodeAnalyzer {
/**
* This will ensure the result is only sent once as multiple images with a valid
* QR code can be sent for analysis.
*/
private var qrCodeRead = false
override lateinit var onQrCodeScanned: (String) -> Unit
override fun analyze(image: ImageProxy) {
if (qrCodeRead) {
return
}
val source = PlanarYUVLuminanceSource(
image.planes[0].buffer.toByteArray(),
image.width,
image.height,
0,
0,
image.width,
image.height,
false,
)
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
try {
val result = MultiFormatReader()
.apply {
setHints(
mapOf(
DecodeHintType.POSSIBLE_FORMATS to arrayListOf(
BarcodeFormat.QR_CODE,
),
),
)
}
.decode(binaryBitmap)
qrCodeRead = true
onQrCodeScanned(result.text)
} catch (e: NotFoundException) {
return
} finally {
image.close()
}
}
}
/**
* This function helps us prepare the byte buffer to be read.
*/
@OmitFromCoverage
private fun ByteBuffer.toByteArray(): ByteArray =
ByteArray(rewind().remaining()).also { get(it) }

View File

@ -3,9 +3,9 @@ package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.feature.qrcodescan.util.FakeQrCodeAnalyzer
import com.bitwarden.ui.util.performCustomAccessibilityAction import com.bitwarden.ui.util.performCustomAccessibilityAction
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util.FakeQrCodeAnalyzer
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify

View File

@ -224,7 +224,6 @@ dependencies {
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization) implementation(libs.kotlinx.serialization)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only // For now we are restricted to running Compose tests for debug builds only
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@ -50,8 +50,6 @@ 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.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzer
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzerImpl
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
import com.bitwarden.ui.platform.base.util.annotatedStringResource import com.bitwarden.ui.platform.base.util.annotatedStringResource
@ -60,6 +58,8 @@ 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.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.QrCodeAnalyzerImpl
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.platform.theme.BitwardenTheme

View File

@ -1,16 +0,0 @@
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util
import androidx.camera.core.ImageAnalysis
import androidx.compose.runtime.Stable
/**
* An interface that is used to help scan QR codes.
*/
@Stable
interface QrCodeAnalyzer : ImageAnalysis.Analyzer {
/**
* The method that is called once the code is scanned.
*/
var onQrCodeScanned: (String) -> Unit
}

View File

@ -1,22 +0,0 @@
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
import androidx.camera.core.ImageProxy
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzer
/**
* A helper class that helps test scan outcomes.
*/
class FakeQrCodeAnalyzer : QrCodeAnalyzer {
override lateinit var onQrCodeScanned: (String) -> Unit
/**
* The result of the scan that will be sent to the ViewModel (or `null` to indicate a
* scanning error.
*/
var scanResult: String? = null
override fun analyze(image: ImageProxy) {
scanResult?.let { onQrCodeScanned.invoke(it) }
}
}

View File

@ -8,6 +8,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import com.bitwarden.authenticator.ui.platform.base.AuthenticatorComposeTest import com.bitwarden.authenticator.ui.platform.base.AuthenticatorComposeTest
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.feature.qrcodescan.util.FakeQrCodeAnalyzer
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk

View File

@ -57,6 +57,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
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.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)
@ -72,6 +73,7 @@ dependencies {
implementation(libs.kotlinx.serialization) implementation(libs.kotlinx.serialization)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only // For now we are restricted to running Compose tests for debug builds only
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
@ -86,6 +88,7 @@ dependencies {
testImplementation(libs.androidx.compose.ui.test) testImplementation(libs.androidx.compose.ui.test)
testFixturesImplementation(libs.androidx.activity.compose) testFixturesImplementation(libs.androidx.activity.compose)
testFixturesImplementation(libs.androidx.camera.camera2)
testFixturesImplementation(libs.androidx.compose.ui.test) testFixturesImplementation(libs.androidx.compose.ui.test)
testFixturesImplementation(libs.androidx.navigation.compose) testFixturesImplementation(libs.androidx.navigation.compose)
testFixturesImplementation(libs.google.hilt.android.testing) testFixturesImplementation(libs.google.hilt.android.testing)

View File

@ -1,4 +1,4 @@
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util package com.bitwarden.ui.platform.feature.qrcodescan.util
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable

View File

@ -1,6 +1,7 @@
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util package com.bitwarden.ui.platform.feature.qrcodescan.util
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import com.bitwarden.annotation.OmitFromCoverage
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType import com.google.zxing.DecodeHintType
@ -14,6 +15,7 @@ import java.nio.ByteBuffer
* A class setup to handle image analysis so that we can use the Zxing library * A class setup to handle image analysis so that we can use the Zxing library
* to scan QR codes and convert them to a string. * to scan QR codes and convert them to a string.
*/ */
@OmitFromCoverage
class QrCodeAnalyzerImpl : QrCodeAnalyzer { class QrCodeAnalyzerImpl : QrCodeAnalyzer {
/** /**
@ -31,7 +33,7 @@ class QrCodeAnalyzerImpl : QrCodeAnalyzer {
val source = PlanarYUVLuminanceSource( val source = PlanarYUVLuminanceSource(
image.planes[0].buffer.toByteArray(), image.planes[0].buffer.toByteArray(),
image.planes[0].rowStride, image.width,
image.height, image.height,
0, 0,
0, 0,
@ -39,12 +41,10 @@ class QrCodeAnalyzerImpl : QrCodeAnalyzer {
image.height, image.height,
false, false,
) )
val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
try { try {
val result = MultiFormatReader().decode( val result = MultiFormatReader().decode(
/* image = */ binaryBitmap, binaryBitmap,
/* hints = */
mapOf( mapOf(
DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE),
DecodeHintType.ALSO_INVERTED to true, DecodeHintType.ALSO_INVERTED to true,
@ -53,7 +53,8 @@ class QrCodeAnalyzerImpl : QrCodeAnalyzer {
qrCodeRead = true qrCodeRead = true
onQrCodeScanned(result.text) onQrCodeScanned(result.text)
} catch (ignored: NotFoundException) { } catch (_: NotFoundException) {
return
} finally { } finally {
image.close() image.close()
} }
@ -63,5 +64,6 @@ class QrCodeAnalyzerImpl : QrCodeAnalyzer {
/** /**
* This function helps us prepare the byte buffer to be read. * This function helps us prepare the byte buffer to be read.
*/ */
@OmitFromCoverage
private fun ByteBuffer.toByteArray(): ByteArray = private fun ByteBuffer.toByteArray(): ByteArray =
ByteArray(rewind().remaining()).also { get(it) } ByteArray(rewind().remaining()).also { get(it) }

View File

@ -1,4 +1,4 @@
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util package com.bitwarden.ui.platform.feature.qrcodescan.util
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy