flashlight icon + bounding boxes

This commit is contained in:
Hadrian Burkhardt
2026-02-13 01:27:37 +01:00
parent 4f3dd5b5e3
commit 88dedef4b6
9 changed files with 469 additions and 45 deletions
@@ -1,23 +1,71 @@
package com.clean.scanner.data.scanner
import android.graphics.Rect
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.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
data class DetectionPoint(
val x: Float,
val y: Float
)
data class DetectionBox(
val left: Float,
val top: Float,
val right: Float,
val bottom: Float,
val corners: List<DetectionPoint> = emptyList()
)
class MlKitBarcodeAnalyzer(
private val isAnalysisEnabled: () -> Boolean = { true },
private val onDetectionStateChanged: (
hasPotential: Boolean,
hasReadable: Boolean,
boxes: List<DetectionBox>,
sourceWidth: Int,
sourceHeight: Int
) -> Unit = { _, _, _, _, _ -> },
private val onDetected: (ScanResult) -> Unit
) : ImageAnalysis.Analyzer, AutoCloseable {
private val scanner = BarcodeScanning.getClient()
private val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.enableAllPotentialBarcodes()
.build()
)
@Volatile
private var processing = false
@Volatile
private var lastHasPotential = false
@Volatile
private var lastHasReadable = false
@Volatile
private var lastBoxes: List<DetectionBox> = emptyList()
@Volatile
private var lastSourceWidth = 0
@Volatile
private var lastSourceHeight = 0
override fun analyze(imageProxy: ImageProxy) {
if (!isAnalysisEnabled() || processing) {
if (!isAnalysisEnabled()) {
publishDetectionState(
hasPotential = false,
hasReadable = false,
boxes = emptyList(),
sourceWidth = 0,
sourceHeight = 0
)
imageProxy.close()
return
}
if (processing) {
imageProxy.close()
return
}
@@ -26,15 +74,58 @@ class MlKitBarcodeAnalyzer(
imageProxy.close()
return
}
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
val sourceWidth = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.height else imageProxy.width
val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
processing = true
scanner.process(image)
.addOnSuccessListener { barcodes ->
val first = barcodes.firstOrNull() ?: return@addOnSuccessListener
val raw = first.rawValue ?: return@addOnSuccessListener
val type = first.valueType.toHumanType()
onDetected(ScanResult(content = raw, type = type))
val readable = barcodes.firstOrNull { !it.rawValue.isNullOrBlank() }
val boxes = barcodes.mapNotNull { barcode ->
val bounds = barcode.boundingBox ?: return@mapNotNull null
val normalized = normalizeBoundingBox(
rect = bounds,
width = imageProxy.width,
height = imageProxy.height,
rotationDegrees = rotationDegrees
) ?: return@mapNotNull null
val normalizedCorners = barcode.cornerPoints?.map { p ->
normalizePoint(
x = p.x.toFloat(),
y = p.y.toFloat(),
width = imageProxy.width,
height = imageProxy.height,
rotationDegrees = rotationDegrees
)
}?.filterNotNull() ?: emptyList()
normalized.copy(corners = normalizedCorners)
}
publishDetectionState(
hasPotential = barcodes.isNotEmpty(),
hasReadable = readable != null,
boxes = boxes,
sourceWidth = sourceWidth,
sourceHeight = sourceHeight
)
if (readable != null) {
onDetected(
ScanResult(
content = readable.rawValue.orEmpty(),
type = readable.valueType.toHumanType()
)
)
}
}
.addOnFailureListener {
publishDetectionState(
hasPotential = false,
hasReadable = false,
boxes = emptyList(),
sourceWidth = 0,
sourceHeight = 0
)
}
.addOnCompleteListener {
processing = false
@@ -61,4 +152,119 @@ class MlKitBarcodeAnalyzer(
override fun close() {
scanner.close()
}
private fun publishDetectionState(
hasPotential: Boolean,
hasReadable: Boolean,
boxes: List<DetectionBox>,
sourceWidth: Int,
sourceHeight: Int
) {
if (
lastHasPotential == hasPotential &&
lastHasReadable == hasReadable &&
lastBoxes == boxes &&
lastSourceWidth == sourceWidth &&
lastSourceHeight == sourceHeight
) return
lastHasPotential = hasPotential
lastHasReadable = hasReadable
lastBoxes = boxes
lastSourceWidth = sourceWidth
lastSourceHeight = sourceHeight
onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight)
}
private fun normalizeBoundingBox(
rect: Rect,
width: Int,
height: Int,
rotationDegrees: Int
): DetectionBox? {
if (width <= 0 || height <= 0) return null
var left = rect.left.toFloat()
var top = rect.top.toFloat()
var right = rect.right.toFloat()
var bottom = rect.bottom.toFloat()
var outWidth = width.toFloat()
var outHeight = height.toFloat()
when (rotationDegrees) {
90 -> {
left = height - rect.bottom.toFloat()
top = rect.left.toFloat()
right = height - rect.top.toFloat()
bottom = rect.right.toFloat()
outWidth = height.toFloat()
outHeight = width.toFloat()
}
180 -> {
left = width - rect.right.toFloat()
top = height - rect.bottom.toFloat()
right = width - rect.left.toFloat()
bottom = height - rect.top.toFloat()
outWidth = width.toFloat()
outHeight = height.toFloat()
}
270 -> {
left = rect.top.toFloat()
top = width - rect.right.toFloat()
right = rect.bottom.toFloat()
bottom = width - rect.left.toFloat()
outWidth = height.toFloat()
outHeight = width.toFloat()
}
}
if (outWidth <= 0f || outHeight <= 0f) return null
return DetectionBox(
left = (left / outWidth).coerceIn(0f, 1f),
top = (top / outHeight).coerceIn(0f, 1f),
right = (right / outWidth).coerceIn(0f, 1f),
bottom = (bottom / outHeight).coerceIn(0f, 1f)
)
}
private fun normalizePoint(
x: Float,
y: Float,
width: Int,
height: Int,
rotationDegrees: Int
): DetectionPoint? {
if (width <= 0 || height <= 0) return null
var outX = x
var outY = y
var outWidth = width.toFloat()
var outHeight = height.toFloat()
when (rotationDegrees) {
90 -> {
outX = height - y
outY = x
outWidth = height.toFloat()
outHeight = width.toFloat()
}
180 -> {
outX = width - x
outY = height - y
outWidth = width.toFloat()
outHeight = height.toFloat()
}
270 -> {
outX = y
outY = width - x
outWidth = height.toFloat()
outHeight = width.toFloat()
}
}
if (outWidth <= 0f || outHeight <= 0f) return null
return DetectionPoint(
x = (outX / outWidth).coerceIn(0f, 1f),
y = (outY / outHeight).coerceIn(0f, 1f)
)
}
}
@@ -6,6 +6,7 @@ import android.view.ScaleGestureDetector
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.core.UseCaseGroup
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
@@ -19,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.clean.scanner.data.scanner.DetectionBox
import com.clean.scanner.data.scanner.MlKitBarcodeAnalyzer
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -32,19 +34,39 @@ fun CameraPreview(
analysisEnabled: Boolean,
torchEnabled: Boolean,
onTorchAvailabilityChanged: (Boolean) -> Unit,
onDetectionStateChanged: (
hasPotential: Boolean,
hasReadable: Boolean,
boxes: List<DetectionBox>,
sourceWidth: Int,
sourceHeight: Int
) -> Unit = { _, _, _, _, _ -> },
onScan: (String, String) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
val mainExecutor = remember(context) { ContextCompat.getMainExecutor(context) }
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 latestOnDetectionStateChanged = rememberUpdatedState(onDetectionStateChanged)
val analyzer = remember {
MlKitBarcodeAnalyzer(
isAnalysisEnabled = { latestAnalysisEnabled.value },
onDetectionStateChanged = { hasPotential, hasReadable, boxes, sourceWidth, sourceHeight ->
mainExecutor.execute {
latestOnDetectionStateChanged.value(
hasPotential,
hasReadable,
boxes,
sourceWidth,
sourceHeight
)
}
},
onDetected = { result -> latestOnScan.value(result.content, result.type) }
)
}
@@ -87,7 +109,18 @@ fun CameraPreview(
setAnalyzer(cameraExecutor, analyzer)
}
val camera = provider.bindToLifecycle(
val camera = previewView.viewPort?.let { viewPort ->
val group = UseCaseGroup.Builder()
.setViewPort(viewPort)
.addUseCase(preview)
.addUseCase(imageAnalysis)
.build()
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
group
)
} ?: provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
@@ -107,6 +140,7 @@ fun CameraPreview(
modifier = modifier,
factory = {
previewView.apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
performClick()
@@ -22,20 +22,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.FlashOff
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -49,6 +54,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
@@ -56,10 +62,12 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.clean.scanner.R
import com.clean.scanner.data.scanner.DetectionBox
import com.clean.scanner.domain.ScanResult
import com.clean.scanner.ui.BatchScanRecord
import com.clean.scanner.ui.components.CameraPreview
@@ -68,10 +76,12 @@ import com.clean.scanner.util.Intents
import com.clean.scanner.util.ScanContentParsers
import com.clean.scanner.util.UrlRiskScorer
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import java.text.DateFormat
import java.util.Date
import kotlin.math.max
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -110,8 +120,21 @@ fun ScannerScreen(
var pendingOpenUrl by remember { mutableStateOf<String?>(null) }
var showImageScanFailed by remember { mutableStateOf(false) }
var showNoCodeInImage by remember { mutableStateOf(false) }
var imageScanCandidates by remember { mutableStateOf<List<ScanResult>>(emptyList()) }
var hasPotentialInView by remember { mutableStateOf(false) }
var hasReadableInView by remember { mutableStateOf(false) }
var detectionBoxes by remember { mutableStateOf<List<DetectionBox>>(emptyList()) }
var detectionSourceWidth by remember { mutableStateOf(0) }
var detectionSourceHeight by remember { mutableStateOf(0) }
val activity = context as? Activity
val imageScanner = remember { BarcodeScanning.getClient() }
val imageScanner = remember {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.enableAllPotentialBarcodes()
.build()
)
}
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
@@ -138,12 +161,21 @@ fun ScannerScreen(
imageScanner.process(image)
.addOnSuccessListener { barcodes ->
val first = barcodes.firstOrNull()
val raw = first?.rawValue
if (raw.isNullOrBlank()) {
showNoCodeInImage = true
} else {
onScan(ScanResult(content = raw, type = first.valueType.toHumanType()))
val candidates = barcodes.mapNotNull { barcode ->
val raw = barcode.rawValue?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
ScanResult(content = raw, type = barcode.valueType.toHumanType())
}.distinctBy { "${it.type}|${it.content}" }
when (candidates.size) {
0 -> showNoCodeInImage = true
1 -> {
if (barcodes.size > 1) {
imageScanCandidates = candidates
} else {
onScan(candidates.first())
}
}
else -> imageScanCandidates = candidates
}
}
.addOnFailureListener {
@@ -194,18 +226,80 @@ fun ScannerScreen(
analysisEnabled = analysisEnabled,
torchEnabled = torchEnabled,
onTorchAvailabilityChanged = { torchAvailable = it },
onDetectionStateChanged = { hasPotential, hasReadable, boxes, sourceWidth, sourceHeight ->
hasPotentialInView = hasPotential
hasReadableInView = hasReadable
detectionBoxes = boxes
detectionSourceWidth = sourceWidth
detectionSourceHeight = sourceHeight
},
onScan = { content, type ->
onScan(ScanResult(content = content, type = type))
}
)
if (detectionBoxes.isNotEmpty()) {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val sourceW = detectionSourceWidth.toFloat()
val sourceH = detectionSourceHeight.toFloat()
if (sourceW <= 0f || sourceH <= 0f) return@Canvas
val scale = max(size.width / sourceW, size.height / sourceH)
val scaledW = sourceW * scale
val scaledH = sourceH * scale
val offsetX = (size.width - scaledW) / 2f
val offsetY = (size.height - scaledH) / 2f
val boxColor = if (hasReadableInView) Color(0xFF4AE3A3) else Color(0xFFFFC857)
detectionBoxes.forEach { box ->
val left = offsetX + (box.left * sourceW * scale)
val top = offsetY + (box.top * sourceH * scale)
val right = offsetX + (box.right * sourceW * scale)
val bottom = offsetY + (box.bottom * sourceH * scale)
val mappedCorners = box.corners.map { p ->
androidx.compose.ui.geometry.Offset(
x = offsetX + (p.x * sourceW * scale),
y = offsetY + (p.y * sourceH * scale)
)
}
if (mappedCorners.size >= 4) {
val outline = Path().apply {
moveTo(mappedCorners.first().x, mappedCorners.first().y)
mappedCorners.drop(1).forEach { pt -> lineTo(pt.x, pt.y) }
close()
}
drawPath(
path = outline,
color = boxColor.copy(alpha = 0.96f),
style = Stroke(width = 4f)
)
} else if (right > left && bottom > top) {
drawRoundRect(
color = boxColor.copy(alpha = 0.95f),
topLeft = androidx.compose.ui.geometry.Offset(left, top),
size = androidx.compose.ui.geometry.Size(right - left, bottom - top),
cornerRadius = CornerRadius(14f, 14f),
style = Stroke(width = 4f)
)
}
}
}
}
Canvas(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(0.7f)
.height(220.dp)
) {
val guideColor = Color(0xFF7CE6C6)
val guideColor = when {
hasReadableInView -> Color(0xFF4AE3A3)
hasPotentialInView -> Color(0xFFFFC857)
else -> Color(0xFF7CE6C6)
}
val cx = size.width / 2f
val cy = size.height / 2f
drawRoundRect(
@@ -225,7 +319,11 @@ fun ScannerScreen(
}
Text(
text = stringResource(R.string.pinch_to_zoom_hint),
text = when {
hasReadableInView -> stringResource(R.string.live_readable_detected)
hasPotentialInView -> stringResource(R.string.live_potential_detected)
else -> stringResource(R.string.pinch_to_zoom_hint)
},
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier
@@ -262,16 +360,21 @@ fun ScannerScreen(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (torchAvailable) {
OverlayToggle(
OverlayIconToggle(
checked = torchEnabled,
onCheckedChange = { torchEnabled = it },
label = stringResource(R.string.flashlight)
label = stringResource(R.string.flashlight),
checkedImageVector = Icons.Default.FlashOn,
uncheckedImageVector = Icons.Default.FlashOff,
showLabel = false
)
}
OverlayToggle(
OverlayIconToggle(
checked = batchMode,
onCheckedChange = onBatchModeChange,
label = stringResource(R.string.batch_mode)
label = stringResource(R.string.batch_mode),
checkedImageVector = Icons.Default.ViewModule,
uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList
)
}
@@ -426,6 +529,56 @@ fun ScannerScreen(
)
}
if (imageScanCandidates.isNotEmpty()) {
AlertDialog(
onDismissRequest = { imageScanCandidates = emptyList() },
title = { Text(stringResource(R.string.image_scan_pick_title, imageScanCandidates.size)) },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = stringResource(R.string.image_scan_pick_subtitle),
modifier = Modifier.padding(bottom = 4.dp)
)
imageScanCandidates.forEach { candidate ->
TextButton(
onClick = {
onScan(candidate)
imageScanCandidates = emptyList()
},
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = candidate.type,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
Text(
text = candidate.content,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = { imageScanCandidates = emptyList() }) {
Text(stringResource(R.string.cancel))
}
}
)
}
if (showImageScanFailed) {
AlertDialog(
onDismissRequest = { showImageScanFailed = false },
@@ -441,12 +594,16 @@ fun ScannerScreen(
}
@Composable
private fun OverlayToggle(
private fun OverlayIconToggle(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
label: String
label: String,
checkedImageVector: androidx.compose.ui.graphics.vector.ImageVector,
uncheckedImageVector: androidx.compose.ui.graphics.vector.ImageVector,
showLabel: Boolean = true
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(
color = Color.Black.copy(alpha = 0.35f),
@@ -454,8 +611,23 @@ private fun OverlayToggle(
)
.padding(horizontal = 10.dp, vertical = 8.dp)
) {
Text(text = label, color = Color.White)
Switch(checked = checked, onCheckedChange = onCheckedChange)
IconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange
) {
Icon(
imageVector = if (checked) checkedImageVector else uncheckedImageVector,
contentDescription = label,
tint = if (checked) Color(0xFF4AE3A3) else Color.White
)
}
if (showLabel) {
Text(
text = label,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}
@@ -2,16 +2,15 @@ package com.clean.scanner.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.provider.Settings
import androidx.core.content.ContextCompat.startActivity
import androidx.core.net.toUri
object Intents {
fun openUrl(context: Context, url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(context, intent, null)
val intent = Intent(Intent.ACTION_VIEW, url.toUri()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
fun shareText(context: Context, text: String) {
@@ -24,27 +23,28 @@ object Intents {
.putExtra(Intent.EXTRA_TEXT, text)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val chooser = Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(context, chooser, null)
context.startActivity(chooser)
}
fun dialPhone(context: Context, number: String) {
val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:${number.trim()}"))
val intent = Intent(Intent.ACTION_DIAL, "tel:${number.trim()}".toUri())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(context, intent, null)
context.startActivity(intent)
}
@Suppress("SpellCheckingInspection")
fun sendSms(context: Context, number: String, message: String?) {
val base = number.trim()
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:$base"))
val intent = Intent(Intent.ACTION_SENDTO, "smsto:$base".toUri())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (!message.isNullOrBlank()) {
intent.putExtra("sms_body", message)
}
startActivity(context, intent, null)
context.startActivity(intent)
}
fun sendEmail(context: Context, email: String, subject: String?, body: String? = null) {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:${email.trim()}"))
val intent = Intent(Intent.ACTION_SENDTO, "mailto:${email.trim()}".toUri())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (!subject.isNullOrBlank()) {
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
@@ -52,12 +52,12 @@ object Intents {
if (!body.isNullOrBlank()) {
intent.putExtra(Intent.EXTRA_TEXT, body)
}
startActivity(context, intent, null)
context.startActivity(intent)
}
fun openWifiSettings(context: Context) {
val intent = Intent(Settings.ACTION_WIFI_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(context, intent, null)
context.startActivity(intent)
}
fun addContact(context: Context, rawContent: String) {
@@ -66,7 +66,7 @@ object Intents {
putExtra(ContactsContract.Intents.Insert.NOTES, rawContent)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(context, intent, null)
context.startActivity(intent)
}
fun addCalendarEvent(context: Context, rawContent: String) {
@@ -76,6 +76,6 @@ object Intents {
putExtra(CalendarContract.Events.DESCRIPTION, rawContent)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(context, intent, null)
context.startActivity(intent)
}
}
+4
View File
@@ -33,6 +33,8 @@
<string name="content_value">Inhalt</string>
<string name="request_camera">Kamera erlauben</string>
<string name="pinch_to_zoom_hint">Zum Zoomen bei kleinen Codes mit zwei Fingern aufziehen</string>
<string name="live_potential_detected">Code erkannt. Ruhig halten oder näher rangehen.</string>
<string name="live_readable_detected">Lesbarer Code erkannt.</string>
<string name="share_history">Historie teilen</string>
<string name="share_txt">TXT</string>
<string name="share_csv">CSV</string>
@@ -43,6 +45,8 @@
<string name="clear_batch">Stapel leeren</string>
<string name="share_batch">Stapel teilen</string>
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
<string name="image_scan_pick_title">%1$d Codes im Bild gefunden</string>
<string name="image_scan_pick_subtitle">Wähle ein Ergebnis aus:</string>
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
<string name="already_scanned">Bereits gescannt</string>
<string name="view_history">Historie anzeigen</string>
+4
View File
@@ -33,6 +33,8 @@
<string name="content_value">Content</string>
<string name="request_camera">Allow camera</string>
<string name="pinch_to_zoom_hint">Pinch to zoom for small codes</string>
<string name="live_potential_detected">Code spotted. Hold steady or move closer.</string>
<string name="live_readable_detected">Readable code detected.</string>
<string name="share_history">Share history</string>
<string name="share_txt">TXT</string>
<string name="share_csv">CSV</string>
@@ -43,6 +45,8 @@
<string name="clear_batch">Clear batch</string>
<string name="share_batch">Share batch</string>
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
<string name="image_scan_pick_title">Found %1$d codes in image</string>
<string name="image_scan_pick_subtitle">Choose a result to use:</string>
<string name="image_scan_failed">Could not read this image. Try another one.</string>
<string name="already_scanned">Already scanned</string>
<string name="view_history">View history</string>