better performance
This commit is contained in:
@@ -4,20 +4,30 @@ import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
|
||||
class MlKitBarcodeAnalyzer(
|
||||
private val isAnalysisEnabled: () -> Boolean = { true },
|
||||
private val onDetected: (ScanResult) -> Unit
|
||||
) : ImageAnalysis.Analyzer {
|
||||
) : ImageAnalysis.Analyzer, AutoCloseable {
|
||||
|
||||
private val scanner = BarcodeScanning.getClient()
|
||||
@Volatile
|
||||
private var processing = false
|
||||
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
if (!isAnalysisEnabled() || processing) {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
|
||||
val mediaImage = imageProxy.image ?: run {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
processing = true
|
||||
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
@@ -26,22 +36,29 @@ class MlKitBarcodeAnalyzer(
|
||||
val type = first.valueType.toHumanType()
|
||||
onDetected(ScanResult(content = raw, type = type))
|
||||
}
|
||||
.addOnCompleteListener { imageProxy.close() }
|
||||
.addOnCompleteListener {
|
||||
processing = false
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toHumanType(): String = when (this) {
|
||||
1 -> "Contact"
|
||||
2 -> "Email"
|
||||
3 -> "ISBN"
|
||||
4 -> "Phone"
|
||||
5 -> "Product"
|
||||
6 -> "SMS"
|
||||
7 -> "Text"
|
||||
8 -> "URL"
|
||||
9 -> "WiFi"
|
||||
10 -> "Geo"
|
||||
11 -> "Calendar"
|
||||
12 -> "Driver license"
|
||||
Barcode.TYPE_CONTACT_INFO -> "Contact"
|
||||
Barcode.TYPE_EMAIL -> "Email"
|
||||
Barcode.TYPE_ISBN -> "ISBN"
|
||||
Barcode.TYPE_PHONE -> "Phone"
|
||||
Barcode.TYPE_PRODUCT -> "Product"
|
||||
Barcode.TYPE_SMS -> "SMS"
|
||||
Barcode.TYPE_TEXT -> "Text"
|
||||
Barcode.TYPE_URL -> "URL"
|
||||
Barcode.TYPE_WIFI -> "WiFi"
|
||||
Barcode.TYPE_GEO -> "Geo"
|
||||
Barcode.TYPE_CALENDAR_EVENT -> "Calendar"
|
||||
Barcode.TYPE_DRIVER_LICENSE -> "Driver license"
|
||||
else -> "Unknown"
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
scanner.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ class ScannerViewModel(
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(ScannerUiState())
|
||||
val uiState: StateFlow<ScannerUiState> = _uiState.asStateFlow()
|
||||
private val recentScanKeySet = LinkedHashSet<String>(200)
|
||||
private val batchKeySet = LinkedHashSet<String>(100)
|
||||
|
||||
fun onScan(result: ScanResult) {
|
||||
val now = nowProvider()
|
||||
@@ -40,14 +42,25 @@ class ScannerViewModel(
|
||||
if (now - current.lastScanTimestamp < 800) return
|
||||
|
||||
val key = "${result.type}|${result.content}"
|
||||
val isDuplicate = current.recentScanKeys.contains(key)
|
||||
val updatedRecent = (listOf(key) + current.recentScanKeys).distinct().take(200)
|
||||
val isDuplicate = key in recentScanKeySet
|
||||
recentScanKeySet.remove(key)
|
||||
recentScanKeySet.add(key)
|
||||
while (recentScanKeySet.size > 200) {
|
||||
recentScanKeySet.remove(recentScanKeySet.first())
|
||||
}
|
||||
val updatedRecent = recentScanKeySet.toList().asReversed()
|
||||
|
||||
_uiState.value = if (current.batchMode) {
|
||||
val updatedBatch = if (current.batchResults.any { "${it.result.type}|${it.result.content}" == key }) {
|
||||
val updatedBatch = if (key in batchKeySet) {
|
||||
current.batchResults
|
||||
} else {
|
||||
(listOf(BatchScanRecord(result = result, timestamp = now)) + current.batchResults).take(100)
|
||||
batchKeySet.add(key)
|
||||
val nextBatch = (listOf(BatchScanRecord(result = result, timestamp = now)) + current.batchResults)
|
||||
.take(100)
|
||||
while (batchKeySet.size > 100) {
|
||||
batchKeySet.remove(batchKeySet.first())
|
||||
}
|
||||
nextBatch
|
||||
}
|
||||
current.copy(
|
||||
lastResult = null,
|
||||
@@ -87,6 +100,7 @@ class ScannerViewModel(
|
||||
}
|
||||
|
||||
fun clearBatchResults() {
|
||||
batchKeySet.clear()
|
||||
_uiState.value = _uiState.value.copy(batchResults = emptyList())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -39,6 +40,15 @@ fun CameraPreview(
|
||||
val previewView = remember { PreviewView(context) }
|
||||
val cameraRef = remember { mutableStateOf<androidx.camera.core.Camera?>(null) }
|
||||
val zoomRatio = remember { mutableStateOf(1f) }
|
||||
val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled)
|
||||
val latestOnScan = rememberUpdatedState(onScan)
|
||||
val analyzer = remember {
|
||||
MlKitBarcodeAnalyzer(
|
||||
isAnalysisEnabled = { latestAnalysisEnabled.value },
|
||||
onDetected = { result -> latestOnScan.value(result.content, result.type) }
|
||||
)
|
||||
}
|
||||
val cameraProviderRef = remember { mutableStateOf<ProcessCameraProvider?>(null) }
|
||||
|
||||
val scaleGestureDetector = remember {
|
||||
ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@@ -56,12 +66,15 @@ fun CameraPreview(
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
cameraProviderRef.value?.unbindAll()
|
||||
analyzer.close()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(analysisEnabled) {
|
||||
LaunchedEffect(lifecycleOwner) {
|
||||
val provider = ProcessCameraProvider.getInstance(context).get()
|
||||
cameraProviderRef.value = provider
|
||||
provider.unbindAll()
|
||||
|
||||
val preview = Preview.Builder().build().apply {
|
||||
@@ -71,13 +84,7 @@ fun CameraPreview(
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build().apply {
|
||||
if (analysisEnabled) {
|
||||
setAnalyzer(cameraExecutor, MlKitBarcodeAnalyzer { result ->
|
||||
onScan(result.content, result.type)
|
||||
})
|
||||
} else {
|
||||
clearAnalyzer()
|
||||
}
|
||||
setAnalyzer(cameraExecutor, analyzer)
|
||||
}
|
||||
|
||||
val camera = provider.bindToLifecycle(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.clean.scanner.ui.screens
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -7,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -14,9 +16,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.clean.scanner.R
|
||||
import com.clean.scanner.util.Intents
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -27,7 +31,10 @@ fun SettingsScreen(
|
||||
onWarningsToggle: (Boolean) -> Unit,
|
||||
onScanFeedbackToggle: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val showDeleteConfirm = remember { mutableStateOf(false) }
|
||||
val showFeatureRequestForm = remember { mutableStateOf(false) }
|
||||
val requesterNeed = remember { mutableStateOf("") }
|
||||
|
||||
if (showDeleteConfirm.value) {
|
||||
AlertDialog(
|
||||
@@ -49,6 +56,51 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showFeatureRequestForm.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showFeatureRequestForm.value = false },
|
||||
title = { Text(stringResource(R.string.feature_request_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = requesterNeed.value,
|
||||
onValueChange = { requesterNeed.value = it },
|
||||
label = { Text(stringResource(R.string.feature_request_details)) }
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val body = buildString {
|
||||
appendLine("Request:")
|
||||
append(requesterNeed.value.trim())
|
||||
}
|
||||
Intents.sendEmail(
|
||||
context = context,
|
||||
email = context.getString(R.string.support_email),
|
||||
subject = context.getString(R.string.feature_request_subject),
|
||||
body = body
|
||||
)
|
||||
showFeatureRequestForm.value = false
|
||||
requesterNeed.value = ""
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.feature_request_sent),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
enabled = requesterNeed.value.isNotBlank()
|
||||
) { Text(stringResource(R.string.send_request)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showFeatureRequestForm.value = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -82,5 +134,9 @@ fun SettingsScreen(
|
||||
Text(text = stringResource(R.string.version))
|
||||
Text(text = stringResource(R.string.licenses))
|
||||
Text(text = stringResource(R.string.contact))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TextButton(onClick = { showFeatureRequestForm.value = true }) {
|
||||
Text(text = stringResource(R.string.feature_request))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +43,15 @@ object Intents {
|
||||
startActivity(context, intent, null)
|
||||
}
|
||||
|
||||
fun sendEmail(context: Context, email: String, subject: String?) {
|
||||
fun sendEmail(context: Context, email: String, subject: String?, body: String? = null) {
|
||||
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:${email.trim()}"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (!subject.isNullOrBlank()) {
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
}
|
||||
if (!body.isNullOrBlank()) {
|
||||
intent.putExtra(Intent.EXTRA_TEXT, body)
|
||||
}
|
||||
startActivity(context, intent, null)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user