This commit is contained in:
Gergely Hegedus 2023-11-18 14:18:58 +02:00
parent be197ec66a
commit cb5bf564fd
54 changed files with 1988 additions and 0 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

78
app/build.gradle Normal file
View file

@ -0,0 +1,78 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'org.fnives.android.qrcodetransfer'
compileSdk 34
defaultConfig {
applicationId "org.fnives.android.qrcodetransfer"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.4'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
def compose_ui_version = '1.5.4'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.5.4'
// qr code
implementation "com.google.zxing:core:3.5.2"
// permission
implementation 'com.google.accompanist:accompanist-permissions:0.32.0'
// camerax
def camerax_version = "1.3.0"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,85 @@
package org.fnives.android.qrcodetransfer
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import org.fnives.android.qrcodetransfer.read.ReadState
import org.fnives.android.qrcodetransfer.read.currentText
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class QRCodeTest {
@Test
fun simple() {
val bitMatrix = SequenceProtocol.createBitMatrix("alma")
val readResult = SequenceProtocol.read(bitMatrix[0].toBitmap().toBinaryBitmap())
Assert.assertEquals(
SequenceProtocol.SequenceInfo.NotSequence("alma"),
readResult?.sequenceInfo
)
Assert.assertEquals(1, bitMatrix.size)
}
@Test
fun emptyThrows() {
Assert.assertThrows(IllegalArgumentException::class.java) {
SequenceProtocol.createBitMatrix("")
}
}
@Test
fun sequence() {
val text = StringBuilder("alma")
repeat(20) {
text.append("alma")
}
val bitMatrix = SequenceProtocol.createBitMatrix(text.toString())
Assert.assertTrue(bitMatrix.size > 1)
val readResults = bitMatrix.map {
val bitmap = it.toBitmap()
SequenceProtocol.read(bitmap.toBinaryBitmap())
}
val endResult = readResults.fold(ReadState(0, emptyMap())) { readState, it ->
val sequenceInfo = (it?.sequenceInfo as SequenceProtocol.SequenceInfo.SequenceElement)
readState.copy(
length = sequenceInfo.length,
parts = readState.parts + mapOf(sequenceInfo.current to sequenceInfo.content)
)
}
Assert.assertEquals(text.toString(), endResult.currentText)
}
@OptIn(ExperimentalEncodingApi::class)
@Test
fun everyASCIICharacterCanBeSend() {
SequenceProtocol.encodeBase64 = true
SequenceProtocol.versionCode = 4
val inputs = (0 until 255).map { it.toChar() }
.map { "$it" }
val results = mutableMapOf<String, String?>()
inputs.forEach {
val bitmap = SequenceProtocol.createBitMatrix(it)[0].toBitmap()
val read = SequenceProtocol.read(bitmap.toBinaryBitmap())
results[it] = read?.sequenceInfo?.content
}
val notMatching = results.entries.filter { it.key != it.value }
println(notMatching)
results.forEach { (input, actual) ->
Assert.assertEquals(input, actual)
}
}
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.QRCodeTransfer"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.QRCodeTransfer">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,85 @@
package org.fnives.android.qrcodetransfer
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.read.ReadQRCode
import org.fnives.android.qrcodetransfer.ui.theme.QRCodeTransferTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
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()
}
}
}
}
}
}
}
}
}
@Composable
fun NavBar(writerSelected: Boolean, setWriterSelected: (Boolean) -> Unit) {
BottomNavigation(Modifier.fillMaxWidth()) {
BottomNavigationItem(
selected = writerSelected,
onClick = { setWriterSelected(true) },
icon = { Icon(Icons.Filled.Create, contentDescription = null) },
label = { Text(stringResource(id = R.string.create_qr_code)) },
)
BottomNavigationItem(
selected = !writerSelected,
onClick = { setWriterSelected(false) },
icon = { Icon(Icons.Filled.Search, contentDescription = null) },
label = { Text(stringResource(id = R.string.read_qr_code)) },
)
}
}

View file

@ -0,0 +1,159 @@
package org.fnives.android.qrcodetransfer
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.EncodeHintType
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.CharacterSetECI
import com.google.zxing.qrcode.QRCodeReader
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.max
import kotlin.math.min
import com.google.zxing.Result as QRCodeResult
// Since zxing only supports reading structured append and I cannot be bothered to modify its encoding
// I simply add header to the "content" of the QR code
// Format: starts with "S://[C][L]" where C is the current index and L is length
// C & L are binary numbers aka character A means 65 (ASCII)
// is this naive? yes, but it seems to be enough for my use case
object SequenceProtocol {
private val writer by lazy { QRCodeWriter() }
private val reader by lazy { QRCodeReader() }
private val maxSizeMap = mutableMapOf<Int, Int>()
private val maxSize get() = maxSizeMap[versionCode] ?: findMaxSize().also { maxSizeMap[versionCode] = it }
private val formatCurrent = 'C'
private val formatLength = 'L'
private val formatPrefix = "S://"
private val format = "${formatPrefix}${formatCurrent}${formatLength}"
var versionCode: Int = 4
set(value) {
field = max(min(value, validVersionCodes.last), validVersionCodes.first)
}
val validVersionCodes = 2 until 40
var encodeBase64: Boolean = false
@OptIn(ExperimentalEncodingApi::class)
@Throws
fun createBitMatrix(text: String): List<BitMatrix> {
val message = base64Encode(text)
if (message.length < maxSize) {
return listOf(encode(message))
}
val contentThatFits = (maxSize - format.length)
val chunks = message.chunked(contentThatFits)
val formatWithLength = format.replace(formatLength, chunks.size.toChar())
val messages = chunks.mapIndexed { index, s ->
val prefix = formatWithLength.replace(formatCurrent, index.toChar())
"${prefix}${s}".also {
println("MYLOG${index} $it")
println("MYLOG$it")
}
}
return messages.map {
encode(it)
}
}
@Throws
fun read(binaryBitmap: BinaryBitmap): ReadResult? {
val result = decode(binaryBitmap) ?: return null
if (!result.text.startsWith(formatPrefix)) {
return ReadResult(SequenceInfo.NotSequence(base64Decode(result.text)), result)
}
val remaining = result.text.drop(formatPrefix.length)
val current = remaining[0]
val length = remaining[1]
val content = base64Decode(remaining.drop(2))
return ReadResult(
SequenceInfo.SequenceElement(
current = current.code,
length = length.code,
content = content,
),
result
)
}
@OptIn(ExperimentalEncodingApi::class)
private fun base64Decode(text: String) =
if (encodeBase64) {
Base64.decode(text).toString(Charsets.UTF_16)
} else {
text
}
@OptIn(ExperimentalEncodingApi::class)
private fun base64Encode(text: String) =
if (encodeBase64) {
Base64.encode(text.toByteArray(Charsets.UTF_16))
} else {
text
}
private fun decode(binaryBitmap: BinaryBitmap) =
try {
reader.decode(binaryBitmap)
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
null
}
private fun encode(message: String): BitMatrix {
return writer.encode(
message,
BarcodeFormat.QR_CODE,
256,
256,
mapOf(
EncodeHintType.CHARACTER_SET to CharacterSetECI.ASCII,
EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.M,
EncodeHintType.QR_VERSION to versionCode,
EncodeHintType.MARGIN to max(versionCode / 2, 3)
)
)
}
/**
* Naive method to find the max size we can fit into our encoding ¯\_()_/¯
*/
fun findMaxSize(): Int {
val msg = StringBuilder("a")
var maxLength: Int? = null
while (maxLength == null) {
try {
encode(msg.toString())
} catch (e: Throwable) {
maxLength = msg.length
}
msg.append("a")
}
return maxLength - 1
}
data class ReadResult(
val sequenceInfo: SequenceInfo,
val underlyingResult: QRCodeResult
)
// whether we are dealing with normal QR Code or "sequenced" one
sealed interface SequenceInfo {
abstract val content: String
data class NotSequence(override val content: String) : SequenceInfo
data class SequenceElement(
val current: Int,
val length: Int,
override val content: String
) : SequenceInfo
}
}

View file

@ -0,0 +1,79 @@
package org.fnives.android.qrcodetransfer
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.content.Intent
import android.content.Intent.CATEGORY_DEFAULT
import android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY
import android.graphics.Bitmap
import android.graphics.Color
import android.icu.text.MessageFormat
import android.net.Uri
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import com.google.zxing.BinaryBitmap
import com.google.zxing.LuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.util.Locale
fun BitMatrix.toBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (x in 0 until width) {
for (y in 0 until height) {
bitmap.setPixel(x, y, if (get(x, y)) Color.BLACK else Color.WHITE)
}
}
return bitmap
}
fun Bitmap.toBinaryBitmap(): BinaryBitmap {
//copy pixel data from the Bitmap into the 'intArray' array
val intArray = IntArray(width * height)
getPixels(intArray, 0, width, 0, 0, width, height)
val source: LuminanceSource = RGBLuminanceSource(width, height, intArray)
return BinaryBitmap(HybridBinarizer(source))
}
fun Context.openAppSettings() {
val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
intent.addCategory(CATEGORY_DEFAULT)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(FLAG_ACTIVITY_NO_HISTORY)
intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
startActivity(intent)
}
fun Int.toOrdinal(): String {
val formatter = MessageFormat("{0,ordinal}", Locale.US)
return formatter.format(arrayOf(this))
}
fun Context.copyToClipboard(text: String) {
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager;
val clipData = ClipData.newPlainText("label", text);
clipboard.setPrimaryClip(clipData)
}
fun String.isMaybeLink() = contains("://")
fun Context.openLink(link: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(FLAG_ACTIVITY_NO_HISTORY)
startActivity(intent)
} catch (ignored: Throwable) {
}
}

View file

@ -0,0 +1,23 @@
package org.fnives.android.qrcodetransfer.create
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import java.util.Base64
import org.fnives.android.qrcodetransfer.R
@Composable
fun Base64EncodeCheckbox(encode: Boolean, setEncode: (Boolean) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = encode, onCheckedChange = setEncode)
Spacer(Modifier.width(4.dp))
Text(stringResource(id = R.string.encode_base64))
}
}

View file

@ -0,0 +1,214 @@
package org.fnives.android.qrcodetransfer.create
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.SequenceProtocol
import org.fnives.android.qrcodetransfer.toBitmap
@Composable
fun CreateQRCode() {
var bitmaps by remember { mutableStateOf(listOf<Bitmap>()) }
var bitmapIndex by remember(bitmaps) { mutableStateOf(0) }
var loading by remember { mutableStateOf(false) }
Column(
Modifier
.padding(24.dp)
.fillMaxSize()
) {
Box(Modifier.weight(1f)) {
if (loading) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
QRCodeCarousel(
bitmaps = bitmaps,
bitmapIndex = bitmapIndex,
setBitmapIndex = { bitmapIndex = it },
)
}
QRCodeContentInput(
bitmaps = bitmaps,
setBitmaps = {
bitmaps.forEach { it.recycle() }
bitmaps = it
},
setLoading = { loading = it }
)
}
}
@Composable
fun QRCodeCarousel(
bitmaps: List<Bitmap>,
bitmapIndex: Int,
setBitmapIndex: (Int) -> Unit
) {
Column(Modifier.fillMaxSize()) {
val imageBitmap = remember(bitmaps, bitmapIndex) {
if (bitmapIndex < bitmaps.size) {
bitmaps[bitmapIndex].asImageBitmap()
} else {
null
}
}
if (imageBitmap != null) {
Image(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
bitmap = imageBitmap,
contentDescription = "",
contentScale = ContentScale.Fit
)
if (bitmaps.size > 1) {
Spacer(Modifier.height(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
val isAfterFirst = bitmapIndex > 0
val isBeforeLast = bitmapIndex + 1 < bitmaps.size
Button(
modifier = Modifier.alpha(if (isAfterFirst) 1f else 0f),
onClick = {
if (isAfterFirst) {
setBitmapIndex(bitmapIndex - 1)
}
}) {
Text(stringResource(id = R.string.previous))
}
Spacer(Modifier.weight(1f))
Text("${bitmapIndex + 1} / ${bitmaps.size}")
Spacer(Modifier.weight(1f))
Button(
modifier = Modifier.alpha(if (isBeforeLast) 1f else 0f),
onClick = {
if (isBeforeLast) {
setBitmapIndex(bitmapIndex + 1)
}
}) {
Text(stringResource(id = R.string.next))
}
}
}
Spacer(Modifier.height(16.dp))
}
}
}
class JobHolder(var job: Job?) {
fun cancel() = job?.cancel()
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun QRCodeContentInput(
bitmaps: List<Bitmap>,
setLoading: (Boolean) -> Unit,
setBitmaps: (List<Bitmap>) -> Unit,
) {
var content by rememberSaveable { mutableStateOf("") }
var number by rememberSaveable { mutableStateOf(SequenceProtocol.versionCode) }
SequenceProtocol.versionCode = number
var encodeBase64 by rememberSaveable { mutableStateOf(SequenceProtocol.encodeBase64) }
SequenceProtocol.encodeBase64 = encodeBase64
val keyboardController = LocalSoftwareKeyboardController.current
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val holder = remember(coroutineScope) { JobHolder(null) }
val createBitmaps = remember(bitmaps, content) {
return@remember fun() {
if (content.isBlank()) return
keyboardController?.hide()
setLoading(true)
holder.cancel()
holder.job = coroutineScope.launch {
val matrix = SequenceProtocol.createBitMatrix(content)
val newBitmaps = matrix.map { it.toBitmap() }
withContext(Dispatchers.Main) {
setBitmaps(newBitmaps)
setLoading(false)
}
}
}
}
val inputScrollState = rememberScrollState()
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.sizeIn(maxHeight = 128.dp)
.verticalScroll(inputScrollState),
label = { Text(stringResource(id = R.string.qr_code_content)) },
value = content,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { createBitmaps() }),
onValueChange = {
setBitmaps(emptyList())
content = it
})
Spacer(modifier = Modifier.height(8.dp))
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
Column {
QRCodeVersionNumberDropdown(number, setVersionNumber = {
number = it
SequenceProtocol.versionCode = it
createBitmaps()
})
Base64EncodeCheckbox(encode = encodeBase64, setEncode = {
encodeBase64 = it
SequenceProtocol.encodeBase64 = it
createBitmaps()
})
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = createBitmaps) {
Text(stringResource(id = R.string.create))
}
}
}

View file

@ -0,0 +1,64 @@
package org.fnives.android.qrcodetransfer.create
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.SequenceProtocol
@Composable
fun QRCodeVersionNumberDropdown(versionNumber: Int, setVersionNumber: (Int) -> Unit) {
val validCodes = SequenceProtocol.validVersionCodes
var expanded by remember {
mutableStateOf(false)
}
Box(modifier = Modifier) {
Text(
stringResource(R.string.version_number, versionNumber),
modifier = Modifier
.border(
1.dp,
color = MaterialTheme.colors.primary,
shape = MaterialTheme.shapes.medium
)
.clickable(onClick = { expanded = true })
.padding(vertical = 4.dp, horizontal = 8.dp)
)
DropdownMenu(
modifier = Modifier.sizeIn(maxHeight = 256.dp),
expanded = expanded,
onDismissRequest = { expanded = false },
) {
validCodes.forEach { value ->
DropdownMenuItem(onClick = {
setVersionNumber(value)
expanded = false
}) {
val text = if (versionNumber == value) {
stringResource(id = R.string.selected_version_number, value)
} else if (value == 14) {
stringResource(id = R.string.recommended_max, value)
} else {
"$value"
}
Text(text = text)
}
}
}
}
}

View file

@ -0,0 +1,35 @@
package org.fnives.android.qrcodetransfer.read
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.copyToClipboard
import org.fnives.android.qrcodetransfer.isMaybeLink
import org.fnives.android.qrcodetransfer.openLink
@Composable
fun ActionRow(readState: ReadState?) {
val context = LocalContext.current
Row(Modifier.alpha(if (readState?.smallestMissingIndex == null) 1f else 0f)) {
Spacer(Modifier.weight(1f))
Button(onClick = { context.copyToClipboard(readState?.currentText.orEmpty()) }) {
Text(stringResource(id = R.string.copy))
}
if (readState?.currentText?.isMaybeLink() == true) {
Spacer(Modifier.width(16.dp))
Button(onClick = { context.openLink(readState.currentText.orEmpty()) }) {
Text(stringResource(id = R.string.open))
}
}
}
}

View file

@ -0,0 +1,112 @@
package org.fnives.android.qrcodetransfer.read
import android.graphics.Bitmap
import androidx.compose.ui.graphics.Color
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.WorkerThread
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import java.time.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
fun interface PreviewProcessor {
@WorkerThread
fun process(image: Bitmap)
}
@Composable
fun CameraView(
interval: Duration,
processImage: PreviewProcessor,
backgroundColor: Color = Color.Black,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val cameraController = remember { LifecycleCameraController(context) }
val bitmapReaderScope = rememberCoroutineScope()
val bitmapStream = MutableStateFlow<Bitmap?>(null)
ImageProcessingEffect(bitmapStream, processImage)
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { context ->
PreviewView(context).apply {
setBackgroundColor(backgroundColor.toArgb())
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
scaleType = PreviewView.ScaleType.FIT_CENTER
implementationMode = PreviewView.ImplementationMode.PERFORMANCE
controller = cameraController
cameraController.bindToLifecycle(lifecycleOwner)
bitmapReaderScope.launch {
while (isActive) {
delay(interval.toMillis())
bitmapStream.value = bitmap
}
}
}
},
onRelease = {
cameraController.unbind()
},
update = {
it.setBackgroundColor(backgroundColor.toArgb())
}
)
}
}
@Composable
fun ImageProcessingEffect(
bitmapStream: Flow<Bitmap?>,
processImage: PreviewProcessor
) {
DisposableEffect(processImage) {
val processScope = CoroutineScope(Dispatchers.IO)
processScope.launch {
bitmapStream
.filterNotNull()
.collectLatest {
processImage.process(it)
}
}
onDispose {
processScope.cancel()
}
}
}

View file

@ -0,0 +1,25 @@
package org.fnives.android.qrcodetransfer.read
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.toOrdinal
@Composable
fun PageInfo(readState: ReadState?) {
val missingIndex = readState?.smallestMissingIndex
if (missingIndex != null) {
Text(
text = stringResource(
id = R.string.next_qr_code,
(missingIndex + 1).toOrdinal()
)
)
Spacer(Modifier.height(16.dp))
}
}

View file

@ -0,0 +1,51 @@
package org.fnives.android.qrcodetransfer.read
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.shouldShowRationale
import org.fnives.android.qrcodetransfer.R
import org.fnives.android.qrcodetransfer.openAppSettings
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionRequester(permissionState: PermissionState) {
var wasRequested by remember { mutableStateOf(false) }
val context = LocalContext.current
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(Modifier.weight(1f))
Text(stringResource(id = R.string.camera_required))
Button(onClick = {
if (wasRequested && !permissionState.status.shouldShowRationale) {
context.openAppSettings()
} else {
permissionState.launchPermissionRequest()
wasRequested = true
}
}) {
if (wasRequested && !permissionState.status.shouldShowRationale) {
Text(stringResource(id = R.string.open_settings))
} else {
Text(stringResource(id = R.string.allow))
}
}
Spacer(Modifier.weight(1f))
}
}

View file

@ -0,0 +1,121 @@
package org.fnives.android.qrcodetransfer.read
import android.graphics.Bitmap
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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 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.toBinaryBitmap
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ReadQRCode() {
val permissionState = rememberPermissionState(permission = "android.permission.CAMERA")
if (permissionState.status == PermissionStatus.Granted) {
QRCodeReader()
} else {
PermissionRequester(permissionState)
}
}
@Composable
fun QRCodeReader() {
var readState by remember { mutableStateOf<ReadState?>(null) }
var encodeBase64 by remember { mutableStateOf<Boolean>(SequenceProtocol.encodeBase64) }
val textScrollState = rememberScrollState()
Column {
Box(
Modifier
.weight(1f)
.fillMaxWidth(),
) {
CameraView(interval = Duration.ofSeconds(1), processImage = {
readState = processImage(it, readState)
})
}
Column {
Base64EncodeCheckbox(encode = encodeBase64, setEncode = {
SequenceProtocol.encodeBase64 = it
readState = null
encodeBase64 = it
})
Column(
Modifier
.padding(24.dp)
.alpha(if (readState == null) 0f else 1f)
) {
PageInfo(readState = readState)
Text(
text = stringResource(id = if (readState?.smallestMissingIndex == null) R.string.content_read else R.string.content_read_partial),
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 = readState?.currentText.orEmpty(),
)
}
ActionRow(readState)
}
}
}
}
fun processImage(bitmap: Bitmap, readState: ReadState?): ReadState? {
val readResult = SequenceProtocol.read(bitmap.toBinaryBitmap()) ?: return readState
if (readResult.sequenceInfo is SequenceProtocol.SequenceInfo.NotSequence) {
return ReadState(
length = 1,
parts = mapOf(0 to readResult.sequenceInfo.content)
)
}
if (readResult.sequenceInfo is SequenceProtocol.SequenceInfo.SequenceElement) {
val currentMap =
mapOf(readResult.sequenceInfo.current to readResult.sequenceInfo.content)
val parts =
if (readResult.sequenceInfo.current == 0 || readState?.length != readResult.sequenceInfo.length) {
currentMap
} else {
currentMap + readState.parts
}
return ReadState(
length = readResult.sequenceInfo.length,
parts = parts
)
}
return readState
}

View file

@ -0,0 +1,15 @@
package org.fnives.android.qrcodetransfer.read
data class ReadState(
val length: Int,
val parts: Map<Int, String>,
)
val ReadState.currentText: String
get() = (0 until length).map {
parts.getOrDefault(it, " ")
}.joinToString(separator = "")
val ReadState.smallestMissingIndex: Int?
get() =
(0 until length).firstOrNull { parts[it] == null }

View file

@ -0,0 +1,8 @@
package org.fnives.android.qrcodetransfer.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

View file

@ -0,0 +1,11 @@
package org.fnives.android.qrcodetransfer.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View file

@ -0,0 +1,47 @@
package org.fnives.android.qrcodetransfer.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Purple200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Purple500
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun QRCodeTransferTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View file

@ -0,0 +1,28 @@
package org.fnives.android.qrcodetransfer.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,22 @@
<resources>
<string name="app_name">QRCodeTransfer</string>
<string name="previous">Previous</string>
<string name="next">Next</string>
<string name="create">Create</string>
<string name="version_number">Version %1$d</string>
<string name="selected_version_number">%1$d (Selected)</string>
<string name="recommended_max">%1$d (Recommended Max)</string>
<string name="encode_base64">Encode in Base64</string>
<string name="camera_required">Camera Permission is required!</string>
<string name="open_settings">Open Settings</string>
<string name="allow">Allow</string>
<string name="content_read_partial">Content read so far:</string>
<string name="content_read">Content read:</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="open">Open</string>
<string name="read_qr_code">Scan QR Code</string>
<string name="create_qr_code">Create QR Code</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.QRCodeTransfer" parent="android:Theme.Material.Light.NoActionBar">
</style>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>