Merge pull request #2 from fknives/support-qrcode-parsing-from-image
Support parsing QRCode data from image intent
This commit is contained in:
commit
60b181fcb6
12 changed files with 327 additions and 27 deletions
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package org.fnives.android.qrcodetransfer
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -26,8 +29,10 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.image.ImageReadQRCode
|
||||
import org.fnives.android.qrcodetransfer.storage.LocalAppPreferencesProvider
|
||||
import org.fnives.android.qrcodetransfer.ui.theme.QRCodeTransferTheme
|
||||
|
||||
|
|
@ -37,28 +42,23 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
setContent {
|
||||
LocalAppPreferencesProvider(this) {
|
||||
LocalIntentTextProvider(intent) {
|
||||
LocalIntentProvider(intent) {
|
||||
QRCodeTransferTheme {
|
||||
var writerSelected by rememberSaveable { mutableStateOf(true) }
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background,
|
||||
) {
|
||||
Scaffold(bottomBar = {
|
||||
NavBar(
|
||||
writerSelected = writerSelected,
|
||||
setWriterSelected = { writerSelected = it })
|
||||
}) {
|
||||
Box(Modifier.padding(it)) {
|
||||
AnimatedContent(targetState = writerSelected) { showWriter ->
|
||||
if (showWriter) {
|
||||
CreateQRCode()
|
||||
} else {
|
||||
ReadQRCode()
|
||||
}
|
||||
val intentImage = LocalIntentImageUri.current
|
||||
if (intentImage != null) {
|
||||
ImageReadQRCode(intentImage,
|
||||
onErrorLoadingFile = {
|
||||
showToast(R.string.could_not_read_content)
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
NormalState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,26 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NormalState() {
|
||||
var writerSelected by rememberSaveable { mutableStateOf(true) }
|
||||
Scaffold(bottomBar = {
|
||||
NavBar(
|
||||
writerSelected = writerSelected,
|
||||
setWriterSelected = { writerSelected = it })
|
||||
}) {
|
||||
Box(Modifier.padding(it)) {
|
||||
AnimatedContent(targetState = writerSelected) { showWriter ->
|
||||
if (showWriter) {
|
||||
CreateQRCode()
|
||||
} else {
|
||||
ReadQRCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) {
|
||||
BottomNavigation(Modifier.fillMaxWidth()) {
|
||||
|
|
@ -85,4 +105,12 @@ fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) {
|
|||
label = { Text(stringResource(id = R.string.read_qr_code)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.showToast(@StringRes stringRes: Int) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(stringRes),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
|
@ -37,11 +37,16 @@ fun BitMatrix.toBitmap(): Bitmap {
|
|||
}
|
||||
|
||||
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
|
||||
val intArray = IntArray(width * height)
|
||||
getPixels(intArray, 0, width, 0, 0, width, height)
|
||||
val intArray = IntArray(bitmap.width * bitmap.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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -15,19 +15,34 @@ import org.fnives.android.qrcodetransfer.R
|
|||
import org.fnives.android.qrcodetransfer.copyToClipboard
|
||||
import org.fnives.android.qrcodetransfer.isMaybeLink
|
||||
import org.fnives.android.qrcodetransfer.openLink
|
||||
import org.fnives.android.qrcodetransfer.read.parsed.DataFormatter
|
||||
|
||||
@Composable
|
||||
fun ActionRow(readState: ReadState?) {
|
||||
val context = LocalContext.current
|
||||
fun ActionRow(readState: ReadState?) =
|
||||
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))
|
||||
Button(onClick = { context.copyToClipboard(readState?.currentText.orEmpty()) }) {
|
||||
Button(onClick = { context.copyToClipboard(formatted) }) {
|
||||
Text(stringResource(id = R.string.copy))
|
||||
}
|
||||
if (readState?.currentText?.isMaybeLink() == true) {
|
||||
if (formatted != parsedContent) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ import androidx.compose.ui.unit.dp
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import java.time.Duration
|
||||
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.parsed.DataFormatter
|
||||
import org.fnives.android.qrcodetransfer.storage.LocalAppPreferences
|
||||
import org.fnives.android.qrcodetransfer.toBinaryBitmap
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ fun QRCodeReader() {
|
|||
modifier = Modifier
|
||||
.heightIn(max = 128.dp)
|
||||
.verticalScroll(textScrollState),
|
||||
text = readState?.currentText.orEmpty(),
|
||||
text = DataFormatter.receivedDataFormatter(readState?.currentText.orEmpty()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.fnives.android.qrcodetransfer.read.parsed
|
||||
|
||||
object DataFormatter {
|
||||
|
||||
fun receivedDataFormatter(data: String): String {
|
||||
return WiFiInfoFormatter.tryToParse(data)?.format() ?: data
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -13,9 +13,11 @@
|
|||
<string name="allow">Allow</string>
|
||||
<string name="content_read_partial">Content read so far:</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="qr_code_content">QRCode Content</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="copy_raw">Copy Raw</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="read_qr_code">Scan QR Code</string>
|
||||
<string name="create_qr_code">Create QR Code</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue