Implement storage of multiple cards
This commit is contained in:
parent
d9218af808
commit
6481878cee
27
.idea/deploymentTargetSelector.xml
generated
27
.idea/deploymentTargetSelector.xml
generated
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="deploymentTargetSelector">
|
|
||||||
<selectionStates>
|
|
||||||
<SelectionState runConfigName="app">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="GreetingPreview">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="MainActivity">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
<DropdownSelection timestamp="2025-08-10T19:19:09.581900351Z">
|
|
||||||
<Target type="DEFAULT_BOOT">
|
|
||||||
<handle>
|
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/kodi/.config/.android/avd/Pixel_3a_API_35.avd" />
|
|
||||||
</handle>
|
|
||||||
</Target>
|
|
||||||
</DropdownSelection>
|
|
||||||
<DialogSelection />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="CardReaderPreview">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
</selectionStates>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@ -9,8 +9,8 @@
|
|||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="/data/src/aimereader" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="/data/src/aimereader/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveExternalAnnotations" value="false" />
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
|
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
@ -49,6 +49,10 @@
|
|||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
|
4
.idea/runConfigurations.xml
generated
4
.idea/runConfigurations.xml
generated
@ -5,8 +5,12 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="/data/src/aimereader" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -2,16 +2,19 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "me.kdcf.aimereader"
|
namespace = "me.kdcf.aimereader"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kdcf.aimereader"
|
applicationId = "me.kdcf.aimereader"
|
||||||
minSdk = 24
|
minSdk = 33
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
|
compileSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@ -20,7 +23,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@ -40,7 +44,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@ -49,6 +52,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.ui.graphics)
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.androidx.material.icons.extended)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
159
app/src/main/java/me/kdcf/aimereader/CardDialog.kt
Normal file
159
app/src/main/java/me/kdcf/aimereader/CardDialog.kt
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package me.kdcf.aimereader
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.InputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.OutputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.insert
|
||||||
|
import androidx.compose.foundation.text.input.maxLength
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
|
import androidx.compose.foundation.text.input.then
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CardDialog(
|
||||||
|
initialCard: SavedCard,
|
||||||
|
titleText: String,
|
||||||
|
confirmText: String,
|
||||||
|
onConfirm: (SavedCard) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
val codeState = rememberTextFieldState(initialText = initialCard.getPadded())
|
||||||
|
val nameState = rememberTextFieldState(initialText = initialCard.getName())
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||||
|
Card (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(350.dp)
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text (
|
||||||
|
titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
OutlinedTextField (
|
||||||
|
state = nameState,
|
||||||
|
label = { Text("Card Name") },
|
||||||
|
placeholder = {
|
||||||
|
// Re-do the spacing, as the code inside the codeState text isn't actually spaced
|
||||||
|
Text(codeState.text.toString().chunked(4).joinToString(" "))
|
||||||
|
},
|
||||||
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
|
)
|
||||||
|
OutlinedTextField (
|
||||||
|
state = codeState,
|
||||||
|
label = { Text("Access Code") },
|
||||||
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
inputTransformation = InputTransformation.maxLength(20).then {
|
||||||
|
if (!asCharSequence().isDigitsOnly()) {
|
||||||
|
revertAllChanges()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outputTransformation = OutputTransformation {
|
||||||
|
if (length > 4) insert(4, " ")
|
||||||
|
if (length > 9) insert(9, " ")
|
||||||
|
if (length > 14) insert(14, " ")
|
||||||
|
if (length > 19) insert(19, " ")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(0.1f))
|
||||||
|
// Display a warning if the code cannot be emulated, meaning that the first 2
|
||||||
|
// bytes are 0x02 0xF3
|
||||||
|
val codeLong = codeState.text.toString().toULong()
|
||||||
|
Row (horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
if (codeLong and 0xFFFF000000000000u == EMULATABLE_CODE_MATCH) {
|
||||||
|
Text(
|
||||||
|
"This Access Code can be emulated.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"This Access Code cannot be emulated.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(0.2f))
|
||||||
|
Row (horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedButton (
|
||||||
|
onClick = {
|
||||||
|
val newCodeLong = Random.nextLong() and EMULATABLE_CODE_MASK.toLong() or EMULATABLE_CODE_MATCH.toLong()
|
||||||
|
codeState.setTextAndPlaceCursorAtEnd(newCodeLong.toString().padStart(20, '0'))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Random Code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row (horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onDismissRequest
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button (
|
||||||
|
enabled = codeState.text.length == 20,
|
||||||
|
|
||||||
|
onClick = {
|
||||||
|
val buffer = ByteBuffer.allocate(Long.SIZE_BYTES)
|
||||||
|
buffer.putLong(codeLong.toLong())
|
||||||
|
onConfirm(SavedCard(buffer.array(), nameState.text.toString()))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(confirmText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CardDialogPreview() {
|
||||||
|
CardDialog(
|
||||||
|
SavedCard(byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07), ""),
|
||||||
|
titleText = "Add Card",
|
||||||
|
confirmText = "Add",
|
||||||
|
onConfirm = {},
|
||||||
|
onDismissRequest = {})
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
package me.kdcf.aimereader
|
package me.kdcf.aimereader
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
@ -12,42 +13,80 @@ import android.nfc.Tag
|
|||||||
import android.nfc.cardemulation.NfcFCardEmulation
|
import android.nfc.cardemulation.NfcFCardEmulation
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Contactless
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.Contactless
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.FilledIconToggleButton
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import me.kdcf.aimereader.ui.theme.AimeReaderTheme
|
import me.kdcf.aimereader.ui.theme.AimeReaderTheme
|
||||||
import java.nio.ByteBuffer
|
import java.io.File
|
||||||
|
|
||||||
|
const val EMULATABLE_CODE_MASK: ULong = 0x02FEFFFFFFFFFFFFu
|
||||||
|
const val EMULATABLE_CODE_MATCH: ULong = 0x02FE000000000000u
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
var intentFiltersArray: Array<IntentFilter>? = null
|
var intentFiltersArray: Array<IntentFilter>? = null
|
||||||
var techListsArray: Array<Array<String>>? = null
|
var techListsArray: Array<Array<String>>? = null
|
||||||
lateinit var pendingIntent: PendingIntent
|
lateinit var pendingIntent: PendingIntent
|
||||||
lateinit var adapter: NfcAdapter
|
lateinit var adapter: NfcAdapter
|
||||||
var canEmulateCard: Boolean = false
|
|
||||||
|
|
||||||
val TAG = "AimeReader"
|
var canEmulateCard: Boolean = false
|
||||||
|
var emulationActive: Boolean = false
|
||||||
|
|
||||||
|
private val TAG = "AimeReader"
|
||||||
|
private val FILENAME = "cards.json"
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -65,20 +104,48 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
var lastCode by remember { mutableStateOf<ByteArray?>(null) }
|
val cardsFile by rememberSaveable {
|
||||||
var scanStatus by remember { mutableStateOf(ScanStatus.WAITING) }
|
val file = File(filesDir, FILENAME)
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
file.writeText("[]") // Empty JSON array
|
||||||
|
}
|
||||||
|
mutableStateOf(file)
|
||||||
|
}
|
||||||
|
var cards by remember {
|
||||||
|
mutableStateOf(Json.decodeFromString<List<SavedCard>>(cardsFile.readText()))
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(lastCode, scanStatus) {
|
var openEditDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var editCardIndex by rememberSaveable { mutableIntStateOf(-1) }
|
||||||
|
var openAddDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var dialogCard by rememberSaveable { mutableStateOf(SavedCard(
|
||||||
|
byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||||
|
)) }
|
||||||
|
|
||||||
|
DisposableEffect(cardsFile, cards) {
|
||||||
val listener = Consumer<Intent> { intent ->
|
val listener = Consumer<Intent> { intent ->
|
||||||
val tagFromIntent: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
|
val tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
|
||||||
Log.i(TAG, tagFromIntent.toString())
|
Log.i(TAG, tagFromIntent.toString())
|
||||||
if (tagFromIntent == null ) {
|
if (tagFromIntent == null) {
|
||||||
Log.i(TAG, "No tag found.")
|
Log.d(TAG, "Got intent without tag")
|
||||||
scanStatus = ScanStatus.FAILURE
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Found tag $tagFromIntent")
|
Log.i(TAG, "Got intent with tag $tagFromIntent")
|
||||||
lastCode = tagFromIntent.id
|
if (tagFromIntent.id.size == Long.SIZE_BYTES) {
|
||||||
scanStatus = ScanStatus.SUCCESS
|
if (openAddDialog || openEditDialog) {
|
||||||
|
val card = SavedCard(tagFromIntent.id, dialogCard.getName())
|
||||||
|
dialogCard = card
|
||||||
|
} else {
|
||||||
|
val card = SavedCard(tagFromIntent.id)
|
||||||
|
cards = cards.plus(card)
|
||||||
|
cardsFile.writeText(Json.encodeToString(cards))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Tag is not a valid Aime/e-Amusement card",
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,15 +155,102 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
var selectedCardIndexForEmulation by rememberSaveable { mutableStateOf(-1) }
|
||||||
|
|
||||||
AimeReaderTheme {
|
AimeReaderTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Scaffold (
|
||||||
Column (
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
snackbarHost = {
|
||||||
verticalArrangement = Arrangement.Center,
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
modifier = Modifier.fillMaxSize()
|
},
|
||||||
) {
|
floatingActionButton = {
|
||||||
CardReader(scanStatus, lastCode)
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
openAddDialog = true
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Add, "Add card")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
|
||||||
|
if (openEditDialog) {
|
||||||
|
CardDialog (
|
||||||
|
dialogCard,
|
||||||
|
titleText = "Edit Card",
|
||||||
|
confirmText = "Edit",
|
||||||
|
onConfirm = {
|
||||||
|
openEditDialog = false
|
||||||
|
cards = cards.mapIndexed { i, prev ->
|
||||||
|
return@mapIndexed if (i == editCardIndex) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cardsFile.writeText(Json.encodeToString(cards))
|
||||||
|
if (editCardIndex == selectedCardIndexForEmulation) {
|
||||||
|
selectedCardIndexForEmulation = -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
openEditDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAddDialog) {
|
||||||
|
CardDialog(
|
||||||
|
titleText = "Add Card",
|
||||||
|
confirmText = "Add",
|
||||||
|
initialCard = SavedCard(
|
||||||
|
byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||||
|
),
|
||||||
|
onConfirm = {
|
||||||
|
openAddDialog = false
|
||||||
|
cards = cards.plus(it)
|
||||||
|
cardsFile.writeText(Json.encodeToString(cards))
|
||||||
|
},
|
||||||
|
onDismissRequest = {
|
||||||
|
openAddDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CardList(cards,
|
||||||
|
emulationAvailable = this@MainActivity.canEmulateCard,
|
||||||
|
deleteCard = {
|
||||||
|
val backedUpCard = cards[it]
|
||||||
|
cards = cards.filterIndexed { i ,_ -> i != it }
|
||||||
|
if (it == selectedCardIndexForEmulation) {
|
||||||
|
selectedCardIndexForEmulation = -1
|
||||||
|
}
|
||||||
|
cardsFile.writeText(Json.encodeToString(cards))
|
||||||
|
scope.launch {
|
||||||
|
val result = snackbarHostState
|
||||||
|
.showSnackbar(
|
||||||
|
message = "Removed card",
|
||||||
|
actionLabel = "Undo",
|
||||||
|
duration = SnackbarDuration.Long,
|
||||||
|
withDismissAction = true
|
||||||
|
)
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
cards = cards.plus(backedUpCard)
|
||||||
|
cardsFile.writeText(Json.encodeToString(cards))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, editCard = {
|
||||||
|
dialogCard = cards[it]
|
||||||
|
editCardIndex = it
|
||||||
|
openEditDialog = true
|
||||||
|
}, selectForEmulation = {
|
||||||
|
selectedCardIndexForEmulation = it
|
||||||
|
},
|
||||||
|
selectedForEmulation = selectedCardIndexForEmulation,
|
||||||
|
innerPadding = innerPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,49 +275,124 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CardReader(status: ScanStatus, code: ByteArray?, modifier: Modifier = Modifier) {
|
fun CardList(
|
||||||
if (status == ScanStatus.WAITING) {
|
cards: List<SavedCard>,
|
||||||
Text(stringResource(R.string.main_scan_title), style = MaterialTheme.typography.titleLarge)
|
emulationAvailable: Boolean,
|
||||||
Text(stringResource(R.string.main_scan_subtitle), style = MaterialTheme.typography.bodyLarge)
|
selectedForEmulation: Int,
|
||||||
} else if (status == ScanStatus.FAILURE || code == null) {
|
deleteCard: (Int) -> Unit,
|
||||||
Text(stringResource(R.string.main_read_failure),
|
editCard: (Int) -> Unit,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
selectForEmulation: (Int) -> Unit,
|
||||||
color = MaterialTheme.colorScheme.error)
|
innerPadding: PaddingValues,
|
||||||
Text(stringResource(R.string.main_read_failture_sub), style = MaterialTheme.typography.bodyLarge)
|
modifier: Modifier = Modifier) {
|
||||||
} else {
|
if (cards.isEmpty()) {
|
||||||
CodeDisplay(code)
|
Column (
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CodeDisplay(code: ByteArray) {
|
|
||||||
val codeNumber = ByteBuffer.wrap(code).long
|
|
||||||
val padded = codeNumber.toString().padStart(20, '0')
|
|
||||||
val spaced = padded.chunked(4).joinToString(" ")
|
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
|
|
||||||
Text(spaced, style = MaterialTheme.typography.titleLarge)
|
|
||||||
Button(onClick = { clipboardManager.setText(AnnotatedString(spaced)) }) { Text(stringResource(R.string.main_copy_code)) }
|
|
||||||
FilledTonalButton(onClick = { clipboardManager.setText(AnnotatedString(padded)) }) { Text(
|
|
||||||
stringResource(R.string.main_copy_without_spaces)
|
|
||||||
) }
|
|
||||||
FilledTonalButton(onClick = { clipboardManager.setText(AnnotatedString(codeNumber.toString())) }) { Text(
|
|
||||||
stringResource(R.string.main_copy_without_zeroes)
|
|
||||||
) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview
|
|
||||||
fun CardReaderPreview(modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
CardReader(ScanStatus.WAITING, null)
|
Text(stringResource(R.string.list_no_cards), style = MaterialTheme.typography.titleLarge)
|
||||||
HorizontalDivider()
|
Text(stringResource(R.string.list_no_cards_body), style = MaterialTheme.typography.bodyLarge)
|
||||||
CardReader(ScanStatus.SUCCESS, ByteArray(8) { x -> x.toByte() })
|
}
|
||||||
HorizontalDivider()
|
} else {
|
||||||
CardReader(ScanStatus.FAILURE, null)
|
LazyColumn (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(innerPadding)
|
||||||
|
) {
|
||||||
|
itemsIndexed(cards) { i, card ->
|
||||||
|
CardDisplay(card,
|
||||||
|
onDelete = { deleteCard(i) },
|
||||||
|
onEdit = { editCard(i) },
|
||||||
|
selectedForEmulation = i == selectedForEmulation,
|
||||||
|
emulationAvailable = emulationAvailable,
|
||||||
|
onSelectForEmulation = { selectForEmulation(i) })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CardDisplay(
|
||||||
|
card: SavedCard,
|
||||||
|
emulationAvailable: Boolean,
|
||||||
|
selectedForEmulation: Boolean,
|
||||||
|
onSelectForEmulation: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(shape = RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(card.getDisplay(),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp).weight(100f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis)
|
||||||
|
Spacer(modifier = Modifier.weight(0.5f))
|
||||||
|
FilledIconButton (
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val text = card.getSpaced()
|
||||||
|
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(text, text)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.ContentCopy, "Copy")
|
||||||
|
}
|
||||||
|
if (emulationAvailable) {
|
||||||
|
FilledIconToggleButton(
|
||||||
|
enabled = card.getCode().toULong() and 0xFFFF000000000000u == EMULATABLE_CODE_MATCH,
|
||||||
|
checked = selectedForEmulation,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) {
|
||||||
|
onSelectForEmulation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (selectedForEmulation) {
|
||||||
|
Icon(Icons.Filled.Contactless, "Emulating")
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Outlined.Contactless, "Emulate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = onEdit
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Edit, "Edit")
|
||||||
|
}
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = onDelete
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Delete, "Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CardDisplayPreview(modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
CardDisplay(SavedCard(byteArrayOf(0x02, 0xFE.toByte(), 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)), true, true, {}, {}, {})
|
||||||
|
CardDisplay(SavedCard(byteArrayOf(0x02, 0xFE.toByte(), 0x12, 0x34, 0x56, 0x78, 0x12, 0x34)), true, false, {}, {}, {})
|
||||||
|
CardDisplay(SavedCard(byteArrayOf(0x02, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)), true, false, {}, {}, {})
|
||||||
|
CardDisplay(SavedCard(byteArrayOf(0x00, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)), false, false, {}, {}, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EmptyCardList(modifier: Modifier = Modifier) {
|
||||||
|
CardList(emptyList(), true, -1, {}, {}, {}, PaddingValues(0.dp), modifier)
|
||||||
|
}
|
||||||
|
35
app/src/main/java/me/kdcf/aimereader/SavedCard.kt
Normal file
35
app/src/main/java/me/kdcf/aimereader/SavedCard.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package me.kdcf.aimereader
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
|
class SavedCard(private var code: ByteArray, private var friendlyName: String = "") : Parcelable {
|
||||||
|
fun getName(): String {
|
||||||
|
return friendlyName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCode(): Long {
|
||||||
|
return ByteBuffer.wrap(code).getLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPadded(): String {
|
||||||
|
return getCode().toString().padStart(20, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSpaced(): String {
|
||||||
|
return getPadded().chunked(4).joinToString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDisplay(): String {
|
||||||
|
if (friendlyName.isEmpty()) {
|
||||||
|
return getSpaced()
|
||||||
|
} else {
|
||||||
|
return friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">Aimeth Readere</string>
|
|
||||||
<string name="emu_service_desc">Aimeth carde emulatorth servicere</string>
|
|
||||||
<string name="main_scan_title">Pleaseth Scaneth thy Carde</string>
|
|
||||||
<string name="main_scan_subtitle">Waitingeth…</string>
|
|
||||||
<string name="main_read_failure">Faileth to Reade the Carde</string>
|
|
||||||
<string name="main_read_failture_sub">Pleaseth trye againe or reporteth a bugue</string>
|
|
||||||
<string name="main_copy_code">Copyeth thy code</string>
|
|
||||||
<string name="main_copy_without_spaces">Copyeth thy code withoute Spacese</string>
|
|
||||||
<string name="main_copy_without_zeroes">Copyeth thy code withoute Zeroes</string>
|
|
||||||
</resources>
|
|
@ -8,4 +8,6 @@
|
|||||||
<string name="main_copy_code">Copy Code</string>
|
<string name="main_copy_code">Copy Code</string>
|
||||||
<string name="main_copy_without_spaces">Copy Without Spaces</string>
|
<string name="main_copy_without_spaces">Copy Without Spaces</string>
|
||||||
<string name="main_copy_without_zeroes">Copy Without Zeroes</string>
|
<string name="main_copy_without_zeroes">Copy Without Zeroes</string>
|
||||||
|
<string name="list_no_cards">No cards.</string>
|
||||||
|
<string name="list_no_cards_body">Scan a card or press the add button.</string>
|
||||||
</resources>
|
</resources>
|
@ -1,13 +1,13 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.0"
|
agp = "8.10.1"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.3.0"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.7.0"
|
||||||
lifecycleRuntimeKtx = "2.6.1"
|
lifecycleRuntimeKtx = "2.9.2"
|
||||||
activityCompose = "1.8.0"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.04.01"
|
composeBom = "2025.08.00"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@ -23,10 +23,13 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.4.0-beta02" }
|
||||||
|
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended-android", version = "1.7.8" }
|
||||||
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json"}
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize" }
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
#Mon Aug 04 17:25:24 GMT 2025
|
#Sun Aug 17 11:26:34 GMT 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
Loading…
x
Reference in New Issue
Block a user