Merge pull request #2 from fknives/support-qrcode-parsing-from-image

Support parsing QRCode data from image intent
This commit is contained in:
Gergely Hegedis 2023-12-18 21:07:31 +01:00 committed by GitHub
commit 60b181fcb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 327 additions and 27 deletions

View file

@ -32,6 +32,7 @@
<data android:mimeType="text/*" /> <data android:mimeType="text/*" />
<data android:mimeType="message/*" /> <data android:mimeType="message/*" />
<data android:mimeType="image/*" />
</intent-filter> </intent-filter>
</activity> </activity>

View file

@ -1,8 +1,11 @@
package org.fnives.android.qrcodetransfer package org.fnives.android.qrcodetransfer
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -26,8 +29,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import org.fnives.android.qrcodetransfer.create.CreateQRCode import org.fnives.android.qrcodetransfer.create.CreateQRCode
import org.fnives.android.qrcodetransfer.intent.LocalIntentTextProvider import org.fnives.android.qrcodetransfer.intent.LocalIntentImageUri
import org.fnives.android.qrcodetransfer.intent.LocalIntentProvider
import org.fnives.android.qrcodetransfer.read.ReadQRCode import org.fnives.android.qrcodetransfer.read.ReadQRCode
import org.fnives.android.qrcodetransfer.read.image.ImageReadQRCode
import org.fnives.android.qrcodetransfer.storage.LocalAppPreferencesProvider import org.fnives.android.qrcodetransfer.storage.LocalAppPreferencesProvider
import org.fnives.android.qrcodetransfer.ui.theme.QRCodeTransferTheme import org.fnives.android.qrcodetransfer.ui.theme.QRCodeTransferTheme
@ -37,14 +42,35 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
LocalAppPreferencesProvider(this) { LocalAppPreferencesProvider(this) {
LocalIntentTextProvider(intent) { LocalIntentProvider(intent) {
QRCodeTransferTheme { QRCodeTransferTheme {
var writerSelected by rememberSaveable { mutableStateOf(true) }
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background, color = MaterialTheme.colors.background,
) { ) {
val intentImage = LocalIntentImageUri.current
if (intentImage != null) {
ImageReadQRCode(intentImage,
onErrorLoadingFile = {
showToast(R.string.could_not_read_content)
finishAfterTransition()
}
)
} else {
NormalState()
}
}
}
}
}
}
}
}
@Composable
fun NormalState() {
var writerSelected by rememberSaveable { mutableStateOf(true) }
Scaffold(bottomBar = { Scaffold(bottomBar = {
NavBar( NavBar(
writerSelected = writerSelected, writerSelected = writerSelected,
@ -61,12 +87,6 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
}
}
}
}
}
@Composable @Composable
fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) { fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) {
@ -86,3 +106,11 @@ fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) {
) )
} }
} }
private fun Activity.showToast(@StringRes stringRes: Int) {
Toast.makeText(
this,
getString(stringRes),
Toast.LENGTH_LONG
).show()
}

View file

@ -37,11 +37,16 @@ fun BitMatrix.toBitmap(): Bitmap {
} }
fun Bitmap.toBinaryBitmap(): BinaryBitmap { fun Bitmap.toBinaryBitmap(): BinaryBitmap {
val bitmap = if (this.config != Bitmap.Config.ARGB_8888) {
this.copy(Bitmap.Config.ARGB_8888, false)
} else {
this
}
//copy pixel data from the Bitmap into the 'intArray' array //copy pixel data from the Bitmap into the 'intArray' array
val intArray = IntArray(width * height) val intArray = IntArray(bitmap.width * bitmap.height)
getPixels(intArray, 0, width, 0, 0, width, height) bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source: LuminanceSource = RGBLuminanceSource(width, height, intArray) val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
return BinaryBitmap(HybridBinarizer(source)) return BinaryBitmap(HybridBinarizer(source))
} }

View file

@ -0,0 +1,11 @@
package org.fnives.android.qrcodetransfer.intent
import android.content.Intent
import androidx.compose.runtime.Composable
@Composable
fun LocalIntentProvider(intent: Intent?, content: @Composable () -> Unit) {
LocalIntentImageUriProvider(intent) {
LocalIntentTextProvider(intent, content)
}
}

View file

@ -0,0 +1,26 @@
package org.fnives.android.qrcodetransfer.intent
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.core.content.IntentCompat
val LocalIntentImageUri = compositionLocalOf<Uri?> {
error("CompositionLocal LocalIntentImageUri not present")
}
@Composable
fun LocalIntentImageUriProvider(intent: Intent?, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalIntentImageUri provides intent?.uri, content = content)
}
private val Intent.uri: Uri?
get() {
return try {
IntentCompat.getParcelableExtra(this, Intent.EXTRA_STREAM, Uri::class.java)
} catch (ignore: Throwable) {
null
}
}

View file

@ -15,19 +15,34 @@ import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.copyToClipboard import org.fnives.android.qrcodetransfer.copyToClipboard
import org.fnives.android.qrcodetransfer.isMaybeLink import org.fnives.android.qrcodetransfer.isMaybeLink
import org.fnives.android.qrcodetransfer.openLink import org.fnives.android.qrcodetransfer.openLink
import org.fnives.android.qrcodetransfer.read.parsed.DataFormatter
@Composable @Composable
fun ActionRow(readState: ReadState?) { fun ActionRow(readState: ReadState?) =
val context = LocalContext.current ActionRow(
parsedContent = readState?.currentText.orEmpty(),
show = readState?.smallestMissingIndex == null
)
Row(Modifier.alpha(if (readState?.smallestMissingIndex == null) 1f else 0f)) { @Composable
fun ActionRow(parsedContent: String, show: Boolean = true) {
val context = LocalContext.current
val formatted = DataFormatter.receivedDataFormatter(parsedContent)
Row(Modifier.alpha(if (show) 1f else 0f)) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button(onClick = { context.copyToClipboard(readState?.currentText.orEmpty()) }) { Button(onClick = { context.copyToClipboard(formatted) }) {
Text(stringResource(id = R.string.copy)) Text(stringResource(id = R.string.copy))
} }
if (readState?.currentText?.isMaybeLink() == true) { if (formatted != parsedContent) {
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Button(onClick = { context.openLink(readState.currentText.orEmpty()) }) { Button(onClick = { context.copyToClipboard(parsedContent) }) {
Text(stringResource(id = R.string.copy_raw))
}
}
if (formatted.isMaybeLink()) {
Spacer(Modifier.width(16.dp))
Button(onClick = { context.openLink(formatted) }) {
Text(stringResource(id = R.string.open)) Text(stringResource(id = R.string.open))
} }
} }

View file

@ -28,10 +28,10 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import java.time.Duration
import org.fnives.android.qrcodetransfer.R import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.SequenceProtocol import org.fnives.android.qrcodetransfer.SequenceProtocol
import org.fnives.android.qrcodetransfer.create.Base64EncodeCheckbox import org.fnives.android.qrcodetransfer.create.Base64EncodeCheckbox
import org.fnives.android.qrcodetransfer.read.parsed.DataFormatter
import org.fnives.android.qrcodetransfer.storage.LocalAppPreferences import org.fnives.android.qrcodetransfer.storage.LocalAppPreferences
import org.fnives.android.qrcodetransfer.toBinaryBitmap import org.fnives.android.qrcodetransfer.toBinaryBitmap
@ -88,7 +88,7 @@ fun QRCodeReader() {
modifier = Modifier modifier = Modifier
.heightIn(max = 128.dp) .heightIn(max = 128.dp)
.verticalScroll(textScrollState), .verticalScroll(textScrollState),
text = readState?.currentText.orEmpty(), text = DataFormatter.receivedDataFormatter(readState?.currentText.orEmpty()),
) )
} }

View file

@ -0,0 +1,29 @@
package org.fnives.android.qrcodetransfer.read.image
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@Composable
fun Uri.toBitmap(): Bitmap? {
val context = LocalContext.current
return remember(this) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, this))
} else {
@Suppress("DEPRECATION")
MediaStore.Images.Media.getBitmap(context.contentResolver, this)
}
} catch (ignore: Throwable) {
null
}
}
}

View file

@ -0,0 +1,113 @@
package org.fnives.android.qrcodetransfer.read.image
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import org.fnives.android.qrcodetransfer.BuildConfig
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.SequenceProtocol
import org.fnives.android.qrcodetransfer.create.Base64EncodeCheckbox
import org.fnives.android.qrcodetransfer.read.ActionRow
import org.fnives.android.qrcodetransfer.read.parsed.DataFormatter
import org.fnives.android.qrcodetransfer.storage.LocalAppPreferences
import org.fnives.android.qrcodetransfer.toBinaryBitmap
@Composable
fun ImageReadQRCode(imageUri: Uri, onErrorLoadingFile: () -> Unit) {
val imageBitmap = imageUri.toBitmap()
LaunchedEffect(imageBitmap) {
if (imageBitmap == null) {
onErrorLoadingFile()
}
}
if (imageBitmap == null) {
return
}
val appPreferences = LocalAppPreferences.current
val encodeBase64 by appPreferences.encodeBase64.collectAsState(initial = SequenceProtocol.encodeBase64)
val imageParsedContent = remember(imageUri, encodeBase64) {
try {
SequenceProtocol.read(imageBitmap.toBinaryBitmap())?.sequenceInfo?.content
} catch (ignored: Throwable) {
if (BuildConfig.DEBUG) {
ignored.printStackTrace()
}
null
}
}
val textScrollState = rememberScrollState()
Column {
Box(
Modifier
.weight(1f)
.fillMaxWidth(),
) {
Image(
modifier = Modifier.fillMaxSize(),
bitmap = imageBitmap.asImageBitmap(),
contentScale = ContentScale.Fit,
contentDescription = null
)
}
Column {
Base64EncodeCheckbox(encode = encodeBase64, setEncode = {
SequenceProtocol.encodeBase64 = it
appPreferences.setEncodeBase64(SequenceProtocol.encodeBase64)
})
Column(
Modifier.padding(24.dp)
) {
if (imageParsedContent == null) {
Text(
text = stringResource(id = R.string.could_not_read_content),
style = LocalTextStyle.current.copy(fontSize = TextUnit(16f, TextUnitType.Sp))
)
} else {
Text(
text = stringResource(id = R.string.content_read),
style = LocalTextStyle.current.copy(fontSize = TextUnit(12f, TextUnitType.Sp))
)
Spacer(Modifier.height(4.dp))
SelectionContainer {
Text(
modifier = Modifier
.heightIn(max = 128.dp)
.verticalScroll(textScrollState),
text = DataFormatter.receivedDataFormatter(imageParsedContent),
)
}
ActionRow(imageParsedContent)
}
}
}
}
}

View file

@ -0,0 +1,8 @@
package org.fnives.android.qrcodetransfer.read.parsed
object DataFormatter {
fun receivedDataFormatter(data: String): String {
return WiFiInfoFormatter.tryToParse(data)?.format() ?: data
}
}

View file

@ -0,0 +1,62 @@
package org.fnives.android.qrcodetransfer.read.parsed
import org.fnives.android.qrcodetransfer.BuildConfig
object WiFiInfoFormatter {
private const val PREFIX = "WIFI:"
private const val EXTRA_KEY = "EXTRA"
private const val NAME_KEY = "S"
private const val PASSWORD_KEY = "P"
private const val SECURITY_KEY = "T"
private const val HIDDEN_KEY = "H"
fun tryToParse(data: String): WifiInfo? {
if (data.startsWith(PREFIX)) {
try {
val result = data.drop(PREFIX.length).split(";").map {
if (it.contains(":")) {
val (key, value) = it.split(":")
key to value
} else {
EXTRA_KEY to it
}
}.toMap()
return WifiInfo(
name = result[NAME_KEY]
?: throw IllegalArgumentException("Could not find name"),
password = result[PASSWORD_KEY]
?: throw IllegalArgumentException("Could not find password"),
security = result[SECURITY_KEY].orEmpty(),
extra = result[EXTRA_KEY].orEmpty(),
hidden = result[HIDDEN_KEY] == "true",
)
} catch (ignored: Throwable) {
if (BuildConfig.DEBUG) {
ignored.printStackTrace()
}
return null
}
} else {
return null
}
}
}
data class WifiInfo(
val name: String,
val security: String,
val password: String,
val hidden: Boolean,
val extra: String,
)
fun WifiInfo.format(): String =
"""
name: $name
password: $password
hidden: $hidden
security: $security
extra: $extra
""".trimIndent()

View file

@ -13,9 +13,11 @@
<string name="allow">Allow</string> <string name="allow">Allow</string>
<string name="content_read_partial">Content read so far:</string> <string name="content_read_partial">Content read so far:</string>
<string name="content_read">Content read:</string> <string name="content_read">Content read:</string>
<string name="could_not_read_content">Content could not be parsed.</string>
<string name="next_qr_code">Navigate to %1$s QR Code</string> <string name="next_qr_code">Navigate to %1$s QR Code</string>
<string name="qr_code_content">QRCode Content</string> <string name="qr_code_content">QRCode Content</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>
<string name="copy_raw">Copy Raw</string>
<string name="open">Open</string> <string name="open">Open</string>
<string name="read_qr_code">Scan QR Code</string> <string name="read_qr_code">Scan QR Code</string>
<string name="create_qr_code">Create QR Code</string> <string name="create_qr_code">Create QR Code</string>