diff --git a/CHANGELOG.md b/CHANGELOG.md index f87c88e..1f01025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added dependency to `keyple-plugin-storage-card-java-api:1.0.0` for support of storage cards +### Build +- Removed dependency to `keyple-gradle` plugin +### Upgraded +- Dokka to `2.0.0` ## [3.0.0] - 2025-02-20 :warning: Major version! diff --git a/bluebird-plugin-mock/build.gradle.kts b/bluebird-plugin-mock/build.gradle.kts index 41e9f70..3028907 100644 --- a/bluebird-plugin-mock/build.gradle.kts +++ b/bluebird-plugin-mock/build.gradle.kts @@ -1,12 +1,16 @@ +import java.time.Year plugins { id("com.android.library") id("kotlin-android") id("kotlin-parcelize") id("org.jetbrains.dokka") id("com.diffplug.spotless") + id("maven-publish") } -val archivesBaseName: String by project +val title: String by project +val copyright: String by project +val currentYear = Year.now().value.toString() android { namespace = "org.calypsonet.keyple.plugin.bluebird.mock" @@ -45,16 +49,6 @@ android { abortOnError = false } - // generate output aar with a qualified name : with version number - libraryVariants.all { - outputs.forEach { output -> - if (output is com.android.build.gradle.internal.api.BaseVariantOutputImpl) { - output.outputFileName = - "$archivesBaseName-${project.version}-mock.${output.outputFile.extension}".replace("-SNAPSHOT", "") - } - } - } - sourceSets { getByName("main").java.srcDirs("src/main/kotlin") getByName("debug").java.srcDirs("src/debug/kotlin") @@ -71,25 +65,111 @@ dependencies { // Bluebird libs compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - // Keyple + // Begin Keyple configuration (generated by 'https://keyple.org/components/overview/configuration-wizard/') implementation("org.eclipse.keyple:keyple-common-java-api:2.0.1") implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.1") implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.0") + // End Keyple configuration + + // Storage card support + api("org.eclipse.keyple:keyple-plugin-storage-card-java-api:1.0.0-SNAPSHOT") { isChanging = true} // Logging implementation("com.jakewharton.timber:timber:5.0.1") } tasks { - dokkaHtml.configure { - dokkaSourceSets { - named("main") { - noAndroidSdkLink.set(false) - includeNonPublic.set(false) - includes.from(files("src/main/kdoc/overview.md")) + dokka { + moduleName.set(rootProject.name) + dokkaSourceSets.main { + includes.from("src/main/kdoc/overview.md") + sourceLink { + localDirectory.set(file("src/main/kotlin")) + remoteUrl("https://github.com/calypsonet/${rootProject.name}") + remoteLineSuffix.set("#L") } } + pluginsConfiguration.html { + footerMessage.set(copyright.replace("YEAR", currentYear)) + } } } -apply(plugin = "org.eclipse.keyple") \ No newline at end of file +val dokkaJar = tasks.register("dokkaJar") { + dependsOn(tasks.dokkaGenerate) + from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory }) + archiveClassifier.set("html-docs") +} + +publishing { + publications { + // This says: defer this block until all of the other stuff has run first. + // This is required since components["release"] is generated by the Android + // plugin in `afterEvaluate` itself, which forces us to do the same. + afterEvaluate { + // Create a new publication called "release". The maven-publish plugin + // creates tasks named publish${name}PublicationTo${target}, where + // ${name} is a capitalized form of the name and ${target} is an output + // repository. By default a MavenLocal target is automatically added, + // which outputs to ~/.m2/repository. + create("mavenJava") { + // Include all artifacts from the "release" component. This is the + // .aar file along with the sources and javadoc .jars. + from(components["release"]) + // add Dokka to the publication + artifact(tasks.named("dokkaJar")) + // Here we configure some properties of the publication (these are + // automatically applied to the pom file). Your library will be + // referenced as ${groupId}:${artifactId}. + groupId = project.group.toString() + artifactId = rootProject.name + version = project.version.toString() + pom { + name.set(title) + description.set(project.description) + url.set("https://github.com/eclipse/${rootProject.name}") + organization { + name.set("Calypso Networks Association") + url.set("https://calypsonet.org/") + } + licenses { + license { + name.set("Eclipse Public License - v 2.0") + url.set("http://www.eclipse.org/legal/epl-2.0") + } + } + developers { + developer { + name.set("Calypso Networks Association Technical Team") + email.set("support-dev@calypsonet.org") + } + } + scm { + connection.set("scm:git:git://github.com/eclipse/${rootProject.name}.git") + developerConnection.set("scm:git:ssh://github.com/eclipse/${rootProject.name}.git") + url.set("https://github.com/eclipse/${rootProject.name}") + } + } + } + } + } +} + +tasks { + dokka { + moduleName.set(rootProject.name) + dokkaSourceSets.main { + includes.from("src/main/kdoc/overview.md") + sourceLink { + localDirectory.set(file("src/main/kotlin")) + remoteUrl("https://github.com/calypsonet/${rootProject.name}") + remoteLineSuffix.set("#L") + } + } + pluginsConfiguration.html { + footerMessage.set(copyright.replace("YEAR", currentYear)) + } + } +} + + diff --git a/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt b/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt index 635d0a4..2d682e4 100644 --- a/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt +++ b/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt @@ -54,7 +54,21 @@ enum class BluebirdContactlessProtocols(private val techValue: Int) { * * @since 3.0.0 */ - INNOVATRON_B_PRIME(0x04); + INNOVATRON_B_PRIME(0x04), + + /** + * STM ST25/SRT512. + * + * @since 3.1.0 + */ + ST25_SRT512(0x08), + + /** + * NXP Mifare Ultralight. + * + * @since 3.1.0 + */ + MIFARE_ULTRALIGHT(0x20); internal fun getValue(): Int = techValue diff --git a/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt b/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt index 31828a1..54c3ae8 100644 --- a/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt +++ b/bluebird-plugin-mock/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt @@ -12,6 +12,7 @@ package org.calypsonet.keyple.plugin.bluebird import android.app.Activity +import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory /** * Provider of [BluebirdPluginFactory] instances. @@ -24,9 +25,12 @@ object BluebirdPluginFactoryProvider { * Provides an instance of [BluebirdPluginFactory]. * * @param activity The activity. - * @since 3.0.0 + * @param apduInterpreterFactory (Optional) The `ApduInterpreterFactory` dedicated to the + * management of storage cards. The interface of this factory is provided by the + * `keyple-plugin-storage-card-java-api` API, its implementation should be provided. + * @since 3.0.0 (single-param usage), 3.1.0 (with APDU interpreter) */ - fun provideFactory(activity: Activity): BluebirdPluginFactory { + fun provideFactory(activity: Activity, apduInterpreterFactory: ApduInterpreterFactory? = null): BluebirdPluginFactory { throw UnsupportedOperationException("Mocked plugin!") } } diff --git a/bluebird-plugin/build.gradle.kts b/bluebird-plugin/build.gradle.kts index a9d1dec..411ee75 100644 --- a/bluebird-plugin/build.gradle.kts +++ b/bluebird-plugin/build.gradle.kts @@ -1,12 +1,16 @@ +import java.time.Year plugins { id("com.android.library") id("kotlin-android") id("kotlin-parcelize") id("org.jetbrains.dokka") id("com.diffplug.spotless") + id("maven-publish") } -val archivesBaseName: String by project +val title: String by project +val copyright: String by project +val currentYear = Year.now().value.toString() android { namespace = "org.calypsonet.keyple.plugin.bluebird" @@ -45,16 +49,6 @@ android { abortOnError = false } - // generate output aar with a qualified name : with version number - libraryVariants.all { - outputs.forEach { output -> - if (output is com.android.build.gradle.internal.api.BaseVariantOutputImpl) { - output.outputFileName = - "$archivesBaseName-${project.version}.${output.outputFile.extension}".replace("-SNAPSHOT", "") - } - } - } - sourceSets { getByName("main").java.srcDirs("src/main/kotlin") getByName("debug").java.srcDirs("src/debug/kotlin") @@ -71,25 +65,111 @@ dependencies { // Bluebird libs compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - // Keyple + // Begin Keyple configuration (generated by 'https://keyple.org/components/overview/configuration-wizard/') implementation("org.eclipse.keyple:keyple-common-java-api:2.0.1") implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.1") implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.0") + // End Keyple configuration + + // Storage card support + api("org.eclipse.keyple:keyple-plugin-storage-card-java-api:1.0.0-SNAPSHOT") { isChanging = true} // Logging implementation("com.jakewharton.timber:timber:5.0.1") } tasks { - dokkaHtml.configure { - dokkaSourceSets { - named("main") { - noAndroidSdkLink.set(false) - includeNonPublic.set(false) - includes.from(files("src/main/kdoc/overview.md")) + dokka { + moduleName.set(rootProject.name) + dokkaSourceSets.main { + includes.from("src/main/kdoc/overview.md") + sourceLink { + localDirectory.set(file("src/main/kotlin")) + remoteUrl("https://github.com/calypsonet/${rootProject.name}") + remoteLineSuffix.set("#L") } } + pluginsConfiguration.html { + footerMessage.set(copyright.replace("YEAR", currentYear)) + } } } -apply(plugin = "org.eclipse.keyple") \ No newline at end of file +val dokkaJar = tasks.register("dokkaJar") { + dependsOn(tasks.dokkaGenerate) + from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory }) + archiveClassifier.set("html-docs") +} + +publishing { + publications { + // This says: defer this block until all of the other stuff has run first. + // This is required since components["release"] is generated by the Android + // plugin in `afterEvaluate` itself, which forces us to do the same. + afterEvaluate { + // Create a new publication called "release". The maven-publish plugin + // creates tasks named publish${name}PublicationTo${target}, where + // ${name} is a capitalized form of the name and ${target} is an output + // repository. By default a MavenLocal target is automatically added, + // which outputs to ~/.m2/repository. + create("mavenJava") { + // Include all artifacts from the "release" component. This is the + // .aar file along with the sources and javadoc .jars. + from(components["release"]) + // add Dokka to the publication + artifact(tasks.named("dokkaJar")) + // Here we configure some properties of the publication (these are + // automatically applied to the pom file). Your library will be + // referenced as ${groupId}:${artifactId}. + groupId = project.group.toString() + artifactId = rootProject.name + version = project.version.toString() + pom { + name.set(title) + description.set(project.description) + url.set("https://github.com/eclipse/${rootProject.name}") + organization { + name.set("Calypso Networks Association") + url.set("https://calypsonet.org/") + } + licenses { + license { + name.set("Eclipse Public License - v 2.0") + url.set("http://www.eclipse.org/legal/epl-2.0") + } + } + developers { + developer { + name.set("Calypso Networks Association Technical Team") + email.set("support-dev@calypsonet.org") + } + } + scm { + connection.set("scm:git:git://github.com/eclipse/${rootProject.name}.git") + developerConnection.set("scm:git:ssh://github.com/eclipse/${rootProject.name}.git") + url.set("https://github.com/eclipse/${rootProject.name}") + } + } + } + } + } +} + +tasks { + dokka { + moduleName.set(rootProject.name) + dokkaSourceSets.main { + includes.from("src/main/kdoc/overview.md") + sourceLink { + localDirectory.set(file("src/main/kotlin")) + remoteUrl("https://github.com/calypsonet/${rootProject.name}") + remoteLineSuffix.set("#L") + } + } + pluginsConfiguration.html { + footerMessage.set(copyright.replace("YEAR", currentYear)) + } + } +} + + diff --git a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdCardReaderAdapter.kt b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdCardReaderAdapter.kt index 17c2260..6b29108 100644 --- a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdCardReaderAdapter.kt +++ b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdCardReaderAdapter.kt @@ -28,16 +28,25 @@ import org.eclipse.keyple.core.plugin.spi.reader.ConfigurableReaderSpi import org.eclipse.keyple.core.plugin.spi.reader.observable.ObservableReaderSpi import org.eclipse.keyple.core.plugin.spi.reader.observable.state.insertion.CardInsertionWaiterAsynchronousSpi import org.eclipse.keyple.core.plugin.spi.reader.observable.state.removal.CardRemovalWaiterBlockingSpi +import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory +import org.eclipse.keyple.core.plugin.storagecard.internal.CommandProcessorApi +import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterFactorySpi +import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterSpi import org.eclipse.keyple.core.util.Assert import org.eclipse.keyple.core.util.HexUtil +import org.json.JSONObject import timber.log.Timber -internal class BluebirdCardReaderAdapter(private val activity: Activity) : +internal class BluebirdCardReaderAdapter( + private val activity: Activity, + private val apduInterpreterFactory: ApduInterpreterFactory? +) : BluebirdCardReader, ObservableReaderSpi, ConfigurableReaderSpi, CardInsertionWaiterAsynchronousSpi, CardRemovalWaiterBlockingSpi, + CommandProcessorApi, BroadcastReceiver() { private companion object { @@ -61,6 +70,20 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : private var vasupMode: Byte? = null private lateinit var waitForCardInsertionAutonomousApi: CardInsertionWaiterAsynchronousApi + private lateinit var uid: ByteArray + + private val apduInterpreter: ApduInterpreterSpi? + + init { + apduInterpreter = + apduInterpreterFactory?.let { + require(it is ApduInterpreterFactorySpi) { + "The provided ApduInterpreterFactory is not an instance of ApduInterpreterFactorySpi" + } + it.createApduInterpreter() + } + apduInterpreter?.setCommandProcessor(this) + } override fun setSkyEcpVasupPayload(vasupPayload: ByteArray) { checkEcpAvailability() @@ -97,9 +120,36 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : if (status < 0) { throw CardIOException("Open physical channel error: {$status: ${getNfcErrorMessage(status)}}") } + + if (currentProtocol == BluebirdContactlessProtocols.ST25_SRT512) { + // specific case for STM SRT512/ST25 + val response = nfcReader.BBextNfcSRT512GetUID() + if (response[0] == 0.toByte()) { + uid = response.copyOfRange(1, 9) + } + } + + currentPowerOnData = + JSONObject() + .put("type", getTypeFromProtocol(currentProtocol!!)) + .put("uid", HexUtil.toHex(uid)) + .toString() + Timber.d("Power on data: $powerOnData") isCardChannelOpen = true } + private fun getTypeFromProtocol(protocol: BluebirdContactlessProtocols): String { + return when (protocol) { + BluebirdContactlessProtocols.ISO_14443_4_A -> "ISO14443-4-A" + BluebirdContactlessProtocols.ISO_14443_4_A_SKY_ECP -> "ISO14443-4-A" + BluebirdContactlessProtocols.ISO_14443_4_B -> "ISO14443-4-B" + BluebirdContactlessProtocols.ISO_14443_4_B_SKY_ECP -> "ISO14443-4-B" + BluebirdContactlessProtocols.INNOVATRON_B_PRIME -> "INNOVATRON-B-PRIME" + BluebirdContactlessProtocols.ST25_SRT512 -> "ISO14443-3-B" + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT -> "ISO14443-3-A" + } + } + override fun closePhysicalChannel() { isCardChannelOpen = false } @@ -115,13 +165,17 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : override fun getPowerOnData(): String = currentPowerOnData ?: "" override fun transmitApdu(apduIn: ByteArray): ByteArray { - val transmitResult = nfcReader.transmit(apduIn) - if (transmitResult.mData != null && transmitResult.mData.size > 256) { - throw CardIOException( - "Transmit APDU error: unexpected response length: ${transmitResult.mData.size}") + try { + return if (apduInterpreter == null) { + transmitIsoApdu(apduIn) + } else { + apduInterpreter.processApdu(apduIn) + } + } catch (e: CardIOException) { + throw e + } catch (e: Exception) { + throw CardIOException("Error while transmitting APDU: ${e.message}", e) } - return transmitResult.mData - ?: throw CardIOException("Transmit APDU error: ${transmitResult.mResult}") } override fun isContactless(): Boolean { @@ -156,6 +210,12 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : BluebirdContactlessProtocols.INNOVATRON_B_PRIME.name -> pollingProtocols = pollingProtocols or BluebirdContactlessProtocols.INNOVATRON_B_PRIME.getValue() + BluebirdContactlessProtocols.ST25_SRT512.name -> + pollingProtocols = + pollingProtocols or BluebirdContactlessProtocols.ST25_SRT512.getValue() + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT.name -> + pollingProtocols = + pollingProtocols or BluebirdContactlessProtocols.MIFARE_ULTRALIGHT.getValue() BluebirdContactlessProtocols.ISO_14443_4_A_SKY_ECP.name -> { checkEcpAvailability() check(vasupMode != ExtNfcReader.ECP.Mode.VASUP_B) { "SKY ECP VASUP type B is set" } @@ -188,6 +248,13 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : BluebirdContactlessProtocols.INNOVATRON_B_PRIME.name -> pollingProtocols = pollingProtocols and BluebirdContactlessProtocols.INNOVATRON_B_PRIME.getValue().inv() + BluebirdContactlessProtocols.ST25_SRT512.name -> + pollingProtocols = + pollingProtocols and BluebirdContactlessProtocols.ST25_SRT512.getValue().inv() + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT.name -> + pollingProtocols = + pollingProtocols and + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT.getValue().inv() BluebirdContactlessProtocols.ISO_14443_4_A_SKY_ECP.name, BluebirdContactlessProtocols.ISO_14443_4_B_SKY_ECP.name -> { checkEcpAvailability() @@ -279,26 +346,50 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : BluebirdContactlessProtocols.fromValue( intent.getIntExtra(ExtNfcReader.Broadcast.EXTNFC_CARD_TYPE_KEY, -1)) Timber.d("Discovered tag with protocol: $currentProtocol") - currentProtocol?.let { - currentPowerOnData = - HexUtil.toHex(intent.getByteArrayExtra(ExtNfcReader.Broadcast.EXTNFC_CARD_DATA_KEY)) - waitForCardInsertionAutonomousApi.onCardInserted() - } + // the following UID may be overwritten later according to the card tech + uid = intent.getByteArrayExtra(ExtNfcReader.Broadcast.EXTNFC_CARD_DATA_KEY) as ByteArray + waitForCardInsertionAutonomousApi.onCardInserted() } } override fun waitForCardRemoval() { - if (!isWaitingForCardRemoval) { - isWaitingForCardRemoval = true + if (isWaitingForCardRemoval || !nfcReader.isConnected) return + isWaitingForCardRemoval = true + try { while (isWaitingForCardRemoval) { - try { - transmitApdu(HexUtil.toByteArray(PING_APDU)) - runBlocking { delay(100) } - } catch (_: CardIOException) { - nfcReader.disconnect() + var isCardRemoved: Boolean + when (currentProtocol) { + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT -> { + val response = nfcReader.BBextNfcMifareRead(0) + isCardRemoved = response == null || response.size == 1 + } + BluebirdContactlessProtocols.ST25_SRT512 -> { + val response = nfcReader.BBextNfcSRT512ReadBlock(0) + if (response == null || response.size == 1) { + nfcReader.BBextNfcSRT512Completion() + isCardRemoved = true + } else { + isCardRemoved = false + } + } + else -> { + try { + transmitApdu(HexUtil.toByteArray(PING_APDU)) + isCardRemoved = false + } catch (_: Exception) { + isCardRemoved = true + } + } + } + if (isCardRemoved) { isWaitingForCardRemoval = false + break } + runBlocking { delay(100) } } + } finally { + nfcReader.disconnect() + isWaitingForCardRemoval = false } } @@ -335,4 +426,76 @@ internal class BluebirdCardReaderAdapter(private val activity: Activity) : else -> "Unknown BB error code: $status" } } + + override fun transmitIsoApdu(apdu: ByteArray): ByteArray { + val transmitResult = nfcReader.transmit(apdu) + if (transmitResult.mData != null && transmitResult.mData.size > 256) { + throw CardIOException( + "Transmit APDU error: unexpected response length: ${transmitResult.mData.size}") + } + return transmitResult.mData + ?: throw CardIOException("Transmit APDU error: ${transmitResult.mResult}") + } + + override fun getUID(): ByteArray? { + return uid + } + + override fun readBlock(blockNumber: Int, length: Int): ByteArray { + when (currentProtocol) { + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT -> { + val response = + nfcReader.BBextNfcMifareRead(blockNumber.toByte()) + ?: throw CardIOException("Read block error: BBextNfcMifareRead returned null") + if (response.size == 17) { + if (response[0] == 0.toByte()) { + return response.copyOfRange(1, 17) + } else { + throw CardIOException( + "Read block error: operation failed with result code ${response[0]}") + } + } else { + throw CardIOException("Read block error: invalid response format") + } + } + BluebirdContactlessProtocols.ST25_SRT512 -> { + val response = + nfcReader.BBextNfcSRT512ReadBlock(blockNumber.toByte()) + ?: throw CardIOException("Read block error: BBextNfcSRT512ReadBlock returned null") + if (response.size == 5) { + if (response[0] == 0.toByte()) { + return response.copyOfRange(1, 5) + } else { + throw CardIOException( + "Read block error: operation failed with result code ${response[0]}") + } + } else { + throw CardIOException("Read block error: invalid response format") + } + } + else -> { + throw CardIOException("Read block error: protocol not supported: $currentProtocol") + } + } + } + + override fun writeBlock(blockNumber: Int, data: ByteArray) { + when (currentProtocol) { + BluebirdContactlessProtocols.MIFARE_ULTRALIGHT -> { + val resultCode = nfcReader.BBextNfcMifareWrite(blockNumber.toByte(), data) + if (resultCode != 0) { + throw CardIOException("Write block error: operation failed with result code $resultCode") + } + } + BluebirdContactlessProtocols.ST25_SRT512 -> { + val resultCode = nfcReader.BBextNfcSRT512WriteBlock(blockNumber.toByte(), data) + if (resultCode != 0) { + throw CardIOException("Write block error: operation failed with result code $resultCode") + } + } + else -> { + throw CardIOException("Write block error: protocol not supported: $currentProtocol") + } + } + } } diff --git a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt index 635d0a4..2d682e4 100644 --- a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt +++ b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdContactlessProtocols.kt @@ -54,7 +54,21 @@ enum class BluebirdContactlessProtocols(private val techValue: Int) { * * @since 3.0.0 */ - INNOVATRON_B_PRIME(0x04); + INNOVATRON_B_PRIME(0x04), + + /** + * STM ST25/SRT512. + * + * @since 3.1.0 + */ + ST25_SRT512(0x08), + + /** + * NXP Mifare Ultralight. + * + * @since 3.1.0 + */ + MIFARE_ULTRALIGHT(0x20); internal fun getValue(): Int = techValue diff --git a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginAdapter.kt b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginAdapter.kt index 7185d77..4a4efed 100644 --- a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginAdapter.kt +++ b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginAdapter.kt @@ -14,18 +14,22 @@ package org.calypsonet.keyple.plugin.bluebird import android.app.Activity import org.eclipse.keyple.core.plugin.spi.PluginSpi import org.eclipse.keyple.core.plugin.spi.reader.ReaderSpi +import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory /** * Implementation of the Bluebird Plugin to handle Bluebird contact and contactless readers. * * @since 2.0.0 */ -internal class BluebirdPluginAdapter(private val activity: Activity) : BluebirdPlugin, PluginSpi { +internal class BluebirdPluginAdapter( + private val activity: Activity, + private val apduInterpreterFactory: ApduInterpreterFactory? +) : BluebirdPlugin, PluginSpi { override fun getName(): String = BluebirdConstants.PLUGIN_NAME override fun searchAvailableReaders(): Set = - setOf(BluebirdSamReaderAdapter(), BluebirdCardReaderAdapter(activity)) + setOf(BluebirdSamReaderAdapter(), BluebirdCardReaderAdapter(activity, apduInterpreterFactory)) override fun onUnregister() { // Do nothing -> all unregisterBroadcastReceiver operations are handled by readers diff --git a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryAdapter.kt b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryAdapter.kt index ab9c36b..58d66eb 100644 --- a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryAdapter.kt +++ b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryAdapter.kt @@ -16,18 +16,22 @@ import org.eclipse.keyple.core.common.CommonApiProperties import org.eclipse.keyple.core.plugin.PluginApiProperties import org.eclipse.keyple.core.plugin.spi.PluginFactorySpi import org.eclipse.keyple.core.plugin.spi.PluginSpi +import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory /** * Implementation of the Bluebird Plugin factory. * * @since 2.0.0 */ -internal class BluebirdPluginFactoryAdapter internal constructor(private val activity: Activity) : - BluebirdPluginFactory, PluginFactorySpi { +internal class BluebirdPluginFactoryAdapter +internal constructor( + private val activity: Activity, + private val apduInterpreterFactory: ApduInterpreterFactory? +) : BluebirdPluginFactory, PluginFactorySpi { override fun getPluginName(): String = BluebirdConstants.PLUGIN_NAME - override fun getPlugin(): PluginSpi = BluebirdPluginAdapter(activity) + override fun getPlugin(): PluginSpi = BluebirdPluginAdapter(activity, apduInterpreterFactory) override fun getCommonApiVersion(): String = CommonApiProperties.VERSION diff --git a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt index c1d5e27..8416a9f 100644 --- a/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt +++ b/bluebird-plugin/src/main/kotlin/org/calypsonet/keyple/plugin/bluebird/BluebirdPluginFactoryProvider.kt @@ -12,6 +12,7 @@ package org.calypsonet.keyple.plugin.bluebird import android.app.Activity +import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory /** * Provider of [BluebirdPluginFactory] instances. @@ -24,9 +25,15 @@ object BluebirdPluginFactoryProvider { * Provides an instance of [BluebirdPluginFactory]. * * @param activity The activity. - * @since 3.0.0 + * @param apduInterpreterFactory (Optional) The `ApduInterpreterFactory` dedicated to the + * management of storage cards. The interface of this factory is provided by the + * `keyple-plugin-storage-card-java-api` API, its implementation should be provided. + * @since 3.0.0 (single-param usage), 3.1.0 (with APDU interpreter) */ - fun provideFactory(activity: Activity): BluebirdPluginFactory { - return BluebirdPluginFactoryAdapter(activity) + fun provideFactory( + activity: Activity, + apduInterpreterFactory: ApduInterpreterFactory? = null + ): BluebirdPluginFactory { + return BluebirdPluginFactoryAdapter(activity, apduInterpreterFactory) } } diff --git a/build.gradle.kts b/build.gradle.kts index 73e67c0..e2d1d6d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,15 @@ plugins { id("com.diffplug.spotless") version "6.25.0" - id("org.jetbrains.dokka") version "1.9.20" + id("org.jetbrains.dokka") version "2.0.0" } buildscript { dependencies { classpath("javax.xml.bind:jaxb-api:2.3.1") classpath("com.sun.xml.bind:jaxb-core:2.3.0.1") - classpath("com.sun.xml.bind:jaxb-impl:2.3.1") + classpath("com.sun.xml.bind:jaxb-impl:2.3.9") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22") classpath("com.android.tools.build:gradle:8.8.1") - classpath("org.eclipse.keyple:keyple-gradle:0.2.+") { isChanging = true } } repositories { diff --git a/gradle.properties b/gradle.properties index 32e9662..fe9094b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,7 @@ group = org.calypsonet.keyple title = Keyple Plugin CNA Bluebird Specific NFC Java Lib description = Keyple add-on to manage Bluebird Specific NFC readers -version = 3.0.0 -archivesBaseName = keyple-plugin-cna-bluebird-specific-nfc-java-lib +version = 3.1.0-SNAPSHOT javaSourceLevel = 1.8 javaTargetLevel = 1.8 @@ -16,7 +15,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx4608m -javadoc.logo = -javadoc.copyright = Copyright \u00a9 Calypso Networks Association https://calypsonet.org/ -sonatype.url = https://s01.oss.sonatype.org -sonar.gradle.skipCompile=true +# Dokka +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +copyright = Copyright \u00a9 YEAR Calypso Networks Association https://calypsonet.org/