diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3668339..d3348c7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/MainActivity.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/MainActivity.kt index b65677b..24e90f6 100644 --- a/app/src/main/java/org/fnives/android/qrcodetransfer/MainActivity.kt +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/MainActivity.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/Util.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/Util.kt index cbb87ad..fe791ae 100644 --- a/app/src/main/java/org/fnives/android/qrcodetransfer/Util.kt +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/Util.kt @@ -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)) } diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntent.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntent.kt new file mode 100644 index 0000000..010bf2f --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntent.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntentImageUri.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntentImageUri.kt new file mode 100644 index 0000000..a0c7aa1 --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/intent/LocalIntentImageUri.kt @@ -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 { + 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 + } + } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/ActionRow.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/ActionRow.kt index 9204b9c..0cbe69e 100644 --- a/app/src/main/java/org/fnives/android/qrcodetransfer/read/ActionRow.kt +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/ActionRow.kt @@ -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)) } } diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/ReadQRCode.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/ReadQRCode.kt index 71c525c..7467b1b 100644 --- a/app/src/main/java/org/fnives/android/qrcodetransfer/read/ReadQRCode.kt +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/ReadQRCode.kt @@ -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()), ) } diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/BitmapFromImageUri.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/BitmapFromImageUri.kt new file mode 100644 index 0000000..c8b0a39 --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/BitmapFromImageUri.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/ImageReadQRCode.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/ImageReadQRCode.kt new file mode 100644 index 0000000..09b1c2a --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/image/ImageReadQRCode.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/DataFormatter.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/DataFormatter.kt new file mode 100644 index 0000000..6d2b421 --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/DataFormatter.kt @@ -0,0 +1,8 @@ +package org.fnives.android.qrcodetransfer.read.parsed + +object DataFormatter { + + fun receivedDataFormatter(data: String): String { + return WiFiInfoFormatter.tryToParse(data)?.format() ?: data + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/WiFiInfoFormatter.kt b/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/WiFiInfoFormatter.kt new file mode 100644 index 0000000..d2ce797 --- /dev/null +++ b/app/src/main/java/org/fnives/android/qrcodetransfer/read/parsed/WiFiInfoFormatter.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2dd2872..6178517 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,9 +13,11 @@ Allow Content read so far: Content read: + Content could not be parsed. Navigate to %1$s QR Code QRCode Content Copy + Copy Raw Open Scan QR Code Create QR Code