initial
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
/.idea/deploymentTargetDropDown.xml
|
||||||
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
3
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/compiler.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="11" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
32
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
10
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
78
app/build.gradle
Normal 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
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -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)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/src/main/java/org/fnives/android/qrcodetransfer/Util.kt
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal 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>
|
||||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||||
6
app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
Normal 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>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
22
app/src/main/res/values/strings.xml
Normal 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>
|
||||||
6
app/src/main/res/values/themes.xml
Normal 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>
|
||||||
13
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||||
9
build.gradle
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
}
|
||||||
|
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '7.4.2' apply false
|
||||||
|
id 'com.android.library' version '7.4.2' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
|
||||||
|
}
|
||||||
23
gradle.properties
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#Sat Nov 18 08:19:02 EET 2023
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
185
gradlew
vendored
Executable file
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
16
settings.gradle
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "QRCodeTransfer"
|
||||||
|
include ':app'
|
||||||