From cdbc3b37bf891c6984b6b806805876dfc06fc73e Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Wed, 14 May 2025 16:43:11 +0200 Subject: [PATCH 1/5] add linting rules on push / release --- .github/workflows/push.yml | 11 ++++++++++- .github/workflows/release.yml | 11 ++++++++++- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index bb71bf6..8974f09 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -55,8 +55,17 @@ jobs: distribution: 'temurin' java-version: '21' + - name: Code Style + run: ./gradlew ktlintCheck + + - name: Suggest code style fixes + if: failure() + run: | + ./gradlew ktlintFormat + git diff + - name: Test - run: ./gradlew clean assemble check + run: ./gradlew assemble check - name: Fingerprints run: ./gradlew checkFingerPrints diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4ea5e5..c79e9e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,8 +46,17 @@ jobs: distribution: 'temurin' java-version: '21' + - name: Code Style + run: ./gradlew ktlintCheck + + - name: Suggest code style fixes + if: failure() + run: | + ./gradlew ktlintFormat + git diff + - name: Test - run: ./gradlew clean assemble check + run: ./gradlew assemble check - name: Assemble all APKs run: ./gradlew createReleaseNotes diff --git a/build.gradle.kts b/build.gradle.kts index 55515b4..11c0e9c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.ktlint) apply false } tasks.register("createReleaseNotes") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d81b635..d39095c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ test-json = "20231013" coroutines = "1.10.1" androidxtest = "1.6.1" softauth = "0.1.2" +ktlint = "12.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -50,3 +51,4 @@ softauth = { group = "io.github.adessose", name = "softauthn", version.ref = "so android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } From f84ef471e71d083e540a8133fa18250aaa071954 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Wed, 14 May 2025 16:55:20 +0200 Subject: [PATCH 2/5] apply plugin in wrapper --- wrapper/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/wrapper/build.gradle.kts b/wrapper/build.gradle.kts index 85c84a7..5cf4f8d 100644 --- a/wrapper/build.gradle.kts +++ b/wrapper/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.compose.compiler) + alias(libs.plugins.ktlint) } android { From 26379436ca0e0a37b1c9d82bf61b95f517e27b1b Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Wed, 14 May 2025 18:09:06 +0200 Subject: [PATCH 3/5] fix: seperate out code style failure steps --- .github/workflows/push.yml | 8 +++++--- .github/workflows/release.yml | 6 ------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 8974f09..4a21ece 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -58,11 +58,13 @@ jobs: - name: Code Style run: ./gradlew ktlintCheck + - name: Generate code style fixes + if: failure() + run: ./gradlew ktlintFormat + - name: Suggest code style fixes if: failure() - run: | - ./gradlew ktlintFormat - git diff + run: git diff - name: Test run: ./gradlew assemble check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c79e9e1..56fc97a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,12 +49,6 @@ jobs: - name: Code Style run: ./gradlew ktlintCheck - - name: Suggest code style fixes - if: failure() - run: | - ./gradlew ktlintFormat - git diff - - name: Test run: ./gradlew assemble check From e50248200de99555e412312b1326d68a506545e3 Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Wed, 14 May 2025 19:32:27 +0200 Subject: [PATCH 4/5] update code style to contain composables and a 2025 line length --- wrapper/build.gradle.kts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/wrapper/build.gradle.kts b/wrapper/build.gradle.kts index 5cf4f8d..4cec0cd 100644 --- a/wrapper/build.gradle.kts +++ b/wrapper/build.gradle.kts @@ -47,7 +47,7 @@ android { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } @@ -61,7 +61,6 @@ android { targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { jvmTarget = "11" } @@ -95,7 +94,6 @@ android { } dependencies { - implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -128,3 +126,15 @@ dependencies { debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) } + +configure { + verbose.set(true) + android.set(true) + + additionalEditorconfig.set( + mapOf( + "max_line_length" to "200", + "ktlint_function_naming_ignore_when_annotated_with" to "Composable", + ), + ) +} From 681d12685c82ff2a4050229fd9ea92bbdd4ca2ed Mon Sep 17 00:00:00 2001 From: Mario Bodemann Date: Wed, 14 May 2025 19:40:37 +0200 Subject: [PATCH 5/5] fix ktlint errors warning huge, but non functional changes. --- wrapper/src/androidTest/java/StartUpTest.kt | 31 +- .../bridging/DebugMenuHandler.kt | 92 ++-- .../SoftwareCredentialsContainer.kt | 139 +++--- .../io/yubicolabs/wwwwallet/Extensions.kt | 11 +- .../io/yubicolabs/wwwwallet/MainActivity.kt | 102 ++-- .../io/yubicolabs/wwwwallet/MainViewModel.kt | 6 +- .../wwwwallet/bluetooth/BleClientHandler.kt | 468 +++++++++--------- .../wwwwallet/bluetooth/BleServerHandler.kt | 318 ++++++------ .../wwwwallet/bluetooth/Extensions.kt | 20 +- .../bluetooth/ServiceCharacteristics.kt | 99 ++-- .../debug/PrintingBluetoothGattCallback.kt | 35 +- .../PrintingBluetoothGattServerCallback.kt | 50 +- .../bluetooth/debug/PrintingScanCallback.kt | 5 +- .../wwwwallet/bridging/JSCodeSnippet.kt | 21 +- .../wwwwallet/bridging/WalletJsBridge.kt | 117 ++--- .../NavigatorCredentialsContainer.kt | 4 +- .../NavigatorCredentialsContainerAndroid.kt | 50 +- .../NavigatorCredentialsContainerYubico.kt | 365 +++++++------- .../yubicolabs/wwwwallet/json/Extensions.kt | 106 ++-- .../wwwwallet/webkit/WalletWebChromeClient.kt | 21 +- .../wwwwallet/webkit/WalletWebViewClient.kt | 65 +-- .../wwwwallet/bridging/DebugMenuHandler.kt | 2 +- .../SoftwareCredentialsContainer.kt | 4 +- .../wwwwallet/bridging/JSCodeSnippetTest.kt | 20 +- 24 files changed, 1130 insertions(+), 1021 deletions(-) diff --git a/wrapper/src/androidTest/java/StartUpTest.kt b/wrapper/src/androidTest/java/StartUpTest.kt index 6ff7c48..20c5066 100644 --- a/wrapper/src/androidTest/java/StartUpTest.kt +++ b/wrapper/src/androidTest/java/StartUpTest.kt @@ -56,20 +56,24 @@ class StartUpTest { val webView = view as WebView activityRule.scenario.onActivity { - val injectionSnippet = JSCodeSnippet.fromRawResource( - context = webView.context, - resource = "injectjs.js", - replacements = listOf( - "JAVASCRIPT_BRIDGE" to JAVASCRIPT_BRIDGE_NAME, - "JAVASCRIPT_VISUALIZE_INJECTION" to "${BuildConfig.VISUALIZE_INJECTION}" + val injectionSnippet = + JSCodeSnippet.fromRawResource( + context = webView.context, + resource = "injectjs.js", + replacements = + listOf( + "JAVASCRIPT_BRIDGE" to JAVASCRIPT_BRIDGE_NAME, + "JAVASCRIPT_VISUALIZE_INJECTION" to "${BuildConfig.VISUALIZE_INJECTION}", + ), ) - ) // add syntax exception handler webView.evaluateJavascript("err = '';window.onerror = (e) => err += String(e);") {} // execute snipped injection, and catch errors - webView.evaluateJavascript("try {\n${injectionSnippet.code}\n} catch (e) {\nerr += 'Exception: ' + JSON.stringify(e);\n}") {} + webView.evaluateJavascript( + "try {\n${injectionSnippet.code}\n} catch (e) {\nerr += 'Exception: ' + JSON.stringify(e);\n}", + ) {} webView.evaluateJavascript("err") { error = it @@ -101,11 +105,12 @@ class StartUpTest { val webView = view as WebView activityRule.scenario.onActivity { webView.evaluateJavascript("navigator.credentials") { - webNavigator = if (it == "null") { - null - } else { - it - } + webNavigator = + if (it == "null") { + null + } else { + it + } latch.countDown() } } diff --git a/wrapper/src/debug/java/io/yubicolabs/funke_explorer/bridging/DebugMenuHandler.kt b/wrapper/src/debug/java/io/yubicolabs/funke_explorer/bridging/DebugMenuHandler.kt index 813736b..027458c 100644 --- a/wrapper/src/debug/java/io/yubicolabs/funke_explorer/bridging/DebugMenuHandler.kt +++ b/wrapper/src/debug/java/io/yubicolabs/funke_explorer/bridging/DebugMenuHandler.kt @@ -28,43 +28,42 @@ class DebugMenuHandler( val context: Context, val showUrlRow: (Boolean) -> Unit, ) { - private var maxSeparatorsCount = 1; - private val actions: Map Unit> = mapOf( - SHOW_URL_ROW to { js -> showUrlRow(true) }, - HIDE_URL_ROW to { js -> showUrlRow(false) }, - - LIST_SEPARATOR * maxSeparatorsCount++ to {}, - - OVERRIDE_HINT_WITH_SECURITY_KEY to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['security-key'])") {} }, - OVERRIDE_HINT_WITH_CLIENT_DEVICE to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['client-device'])") {} }, - OVERRIDE_HINT_WITH_EMULATOR to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['emulator'])") {} }, - DO_NOT_OVERRIDE_HINT to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints([])") {} }, - - LIST_SEPARATOR * maxSeparatorsCount++ to {}, - - SEND_FEEDBACK to { js -> - js("$JAVASCRIPT_BRIDGE_NAME.__captured_logs__") { logsJson -> - val jsonArray = JSONArray(logsJson) - val logs = jsonArray.toList().map { "$it" } - val body = createIssueBody(logs, Int.MAX_VALUE) - val title = "Wwwwallet wrapper issue" + private var maxSeparatorsCount = 1 + private val actions: Map Unit> = + mapOf( + SHOW_URL_ROW to { js -> showUrlRow(true) }, + HIDE_URL_ROW to { js -> showUrlRow(false) }, + LIST_SEPARATOR * maxSeparatorsCount++ to {}, + OVERRIDE_HINT_WITH_SECURITY_KEY to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['security-key'])") {} }, + OVERRIDE_HINT_WITH_CLIENT_DEVICE to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['client-device'])") {} }, + OVERRIDE_HINT_WITH_EMULATOR to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints(['emulator'])") {} }, + DO_NOT_OVERRIDE_HINT to { it("$JAVASCRIPT_BRIDGE_NAME.overrideHints([])") {} }, + LIST_SEPARATOR * maxSeparatorsCount++ to {}, + SEND_FEEDBACK to { js -> + js("$JAVASCRIPT_BRIDGE_NAME.__captured_logs__") { logsJson -> + val jsonArray = JSONArray(logsJson) + val logs = jsonArray.toList().map { "$it" } + val body = createIssueBody(logs, Int.MAX_VALUE) + val title = "Wwwwallet wrapper issue" // TODO: Once Github is public, move over from email to github issue creation. // val uri = // "https://github.com/wwWallet/wwwallet-android-wrapper/issues/new?title=${title}&body=${body.urlSafe()}".toUri() // context.startActivity(Intent(Intent.ACTION_VIEW, uri)) - val intent = Intent(Intent.ACTION_SEND).apply { - setType("text/html") - putExtra(Intent.EXTRA_EMAIL, arrayOf("mario.bodemann@yubico.com")) - putExtra(Intent.EXTRA_SUBJECT, title) - putExtra(Intent.EXTRA_HTML_TEXT, body) - putExtra(Intent.EXTRA_TEXT, body) // fallback - } + val intent = + Intent(Intent.ACTION_SEND).apply { + setType("text/html") + putExtra(Intent.EXTRA_EMAIL, arrayOf("mario.bodemann@yubico.com")) + putExtra(Intent.EXTRA_SUBJECT, title) + putExtra(Intent.EXTRA_HTML_TEXT, body) + putExtra(Intent.EXTRA_TEXT, body) // fallback + } - context.startActivity(intent) } - }, - ) + context.startActivity(intent) + } + }, + ) fun onMenuOpened(jsExecutor: JSExecutor) { jsExecutor("console.log('Developer encountered.')") {} @@ -74,7 +73,7 @@ class DebugMenuHandler( AlertDialog.Builder(context, theme) .setTitle("Debug Menu") .setItems( - items + items, ) { dialog, which -> val key = items[which] if (key in actions) { @@ -96,26 +95,29 @@ class DebugMenuHandler( } } -private operator fun String.times(times: Int): String = - (0 until times).joinToString(separator = "") { this } +private operator fun String.times(times: Int): String = (0 until times).joinToString(separator = "") { this } -private fun createIssueBody(logs: List, maxLogLineCount: Int = 50): String { +private fun createIssueBody( + logs: List, + maxLogLineCount: Int = 50, +): String { val digits = log10(logs.size.toFloat()).nextUp().toInt() + 1 // truncate log to max lines (otherwise request to github becomes to big) - val truncatedLogs = if (logs.size > maxLogLineCount) { - logs.takeLast(maxLogLineCount) - } else { - logs - } + val truncatedLogs = + if (logs.size > maxLogLineCount) { + logs.takeLast(maxLogLineCount) + } else { + logs + } val truncated = truncatedLogs.size < logs.size - val truncatedOffset = if (truncated) { - logs.size - truncatedLogs.size - } else { - 0 - } - + val truncatedOffset = + if (truncated) { + logs.size - truncatedLogs.size + } else { + 0 + } return """Hey wwwallet team, diff --git a/wrapper/src/debug/java/io/yubicolabs/funke_explorer/credentials/SoftwareCredentialsContainer.kt b/wrapper/src/debug/java/io/yubicolabs/funke_explorer/credentials/SoftwareCredentialsContainer.kt index 497defa..d82022d 100644 --- a/wrapper/src/debug/java/io/yubicolabs/funke_explorer/credentials/SoftwareCredentialsContainer.kt +++ b/wrapper/src/debug/java/io/yubicolabs/funke_explorer/credentials/SoftwareCredentialsContainer.kt @@ -17,7 +17,6 @@ import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.data.UserVerificationRequirement import de.adesso.softauthn.Authenticators import de.adesso.softauthn.CredentialsContainer -import com.yubico.webauthn.data.ByteArray as YubiBytes import de.adesso.softauthn.Origin import de.adesso.softauthn.authenticator.WebAuthnAuthenticator import io.yubicolabs.wwwwallet.BuildConfig @@ -29,17 +28,17 @@ import java.net.URL import java.util.SortedSet import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.jvm.optionals.getOrNull - +import com.yubico.webauthn.data.ByteArray as YubiBytes internal class SoftwareCredentialsContainer : NavigatorCredentialsContainer { private val authenticator: WebAuthnAuthenticator = Authenticators.yubikey5Nfc().build() private val origin = URL(BuildConfig.BASE_URL).toOrigin() - private val credentials = CredentialsContainer(origin, listOf(authenticator)); + private val credentials = CredentialsContainer(origin, listOf(authenticator)) override fun create( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) = try { val publicKey = options.getJSONObject("publicKey") if (publicKey.has("hints")) { @@ -58,7 +57,7 @@ internal class SoftwareCredentialsContainer : NavigatorCredentialsContainer { override fun get( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) = try { val credential = credentials.get(options.getJSONObject("publicKey").toPKCRO()) val response = credential.toAssertionJson() @@ -68,19 +67,18 @@ internal class SoftwareCredentialsContainer : NavigatorCredentialsContainer { } } - private fun URL.toOrigin() = Origin("https", host, -1, null) -private fun JSONObject.toPKCCO(): PublicKeyCredentialCreationOptions = - PublicKeyCredentialCreationOptions.fromJson(toString()) +private fun JSONObject.toPKCCO(): PublicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions.fromJson(toString()) private fun JSONObject.toPKCRO(): PublicKeyCredentialRequestOptions { val challenge = YubiBytes.fromBase64Url(getString("challenge")) - val builder = PublicKeyCredentialRequestOptions.builder() - .challenge(challenge) - .rpId(getString("rpId")) - .allowCredentials(getJSONArray("allowCredentials").toPublicKeyCredentialDescriptors()) + val builder = + PublicKeyCredentialRequestOptions.builder() + .challenge(challenge) + .rpId(getString("rpId")) + .allowCredentials(getJSONArray("allowCredentials").toPublicKeyCredentialDescriptors()) if (has("userVerification")) { builder.userVerification(getString("userVerification").toUserVerificationRequirement()) @@ -89,12 +87,13 @@ private fun JSONObject.toPKCRO(): PublicKeyCredentialRequestOptions { return builder.build() } -private fun String.toUserVerificationRequirement(): UserVerificationRequirement = when (this) { - "discouraged" -> UserVerificationRequirement.DISCOURAGED - "preferred" -> UserVerificationRequirement.PREFERRED - "required" -> UserVerificationRequirement.REQUIRED - else -> UserVerificationRequirement.REQUIRED -} +private fun String.toUserVerificationRequirement(): UserVerificationRequirement = + when (this) { + "discouraged" -> UserVerificationRequirement.DISCOURAGED + "preferred" -> UserVerificationRequirement.PREFERRED + "required" -> UserVerificationRequirement.REQUIRED + else -> UserVerificationRequirement.REQUIRED + } private fun JSONArray.toPublicKeyCredentialDescriptors(): List = toList().mapNotNull { @@ -127,33 +126,35 @@ private fun PublicKeyCredential entry.value != null } + ).filter { entry -> entry.value != null }, ) } -private fun AuthenticatorAssertionResponse.toJson(): JSONObject = JSONObject( - mapOf( - "authenticatorData" to authenticatorData.base64Url, - "clientDataJSON" to clientDataJSON.base64Url, - "signature" to signature.base64Url, - "userHandle" to userHandle.getOrNull()?.base64Url, - "clientData" to clientData.toJson(), - ).filter { entry -> entry.value != null } -) - -private fun ClientAssertionExtensionOutputs.toJson(): JSONObject = JSONObject( - mapOf( - "appid" to appid.getOrNull(), - "largeBlob" to largeBlob.getOrNull()?.toJson() - ).filter { entry -> entry.value != null } -) +private fun AuthenticatorAssertionResponse.toJson(): JSONObject = + JSONObject( + mapOf( + "authenticatorData" to authenticatorData.base64Url, + "clientDataJSON" to clientDataJSON.base64Url, + "signature" to signature.base64Url, + "userHandle" to userHandle.getOrNull()?.base64Url, + "clientData" to clientData.toJson(), + ).filter { entry -> entry.value != null }, + ) + +private fun ClientAssertionExtensionOutputs.toJson(): JSONObject = + JSONObject( + mapOf( + "appid" to appid.getOrNull(), + "largeBlob" to largeBlob.getOrNull()?.toJson(), + ).filter { entry -> entry.value != null }, + ) private fun Extensions.LargeBlob.LargeBlobAuthenticationOutput.toJson(): JSONObject = JSONObject( mapOf( "blob" to blob.getOrNull()?.base64Url, "written" to written.getOrNull(), - ).filter { entry -> entry.value != null } + ).filter { entry -> entry.value != null }, ) private fun PublicKeyCredential?.toAttestationJson(): JSONObject = @@ -170,37 +171,41 @@ private fun PublicKeyCredential entry.value != null } + ).filter { entry -> entry.value != null }, ) } -private fun ClientRegistrationExtensionOutputs.toJson() = JSONObject( - mapOf( - "appidExclude" to appidExclude.getOrNull(), - "credProps" to credProps.getOrNull(), - "largeBlob" to largeBlob.getOrNull(), - ).filter { entry -> entry.value != null } -) - -private fun AuthenticatorAttestationResponse.toJson() = JSONObject( - mapOf( - "attestationObject" to attestationObject.base64Url, - "clientData" to clientData.toJson(), - "clientDataJSON" to clientDataJSON.base64Url, - "transports" to transports.toJson(), - ).filter { entry -> entry.value != null } -) - -private fun CollectedClientData.toJson() = JSONObject( - mapOf( - "challenge" to challenge.base64Url, - "origin" to origin, - "type" to type, - ).filter { entry -> entry.value != null } -) - -private fun SortedSet.toJson() = JSONArray( - mapNotNull { - it.id - } -) +private fun ClientRegistrationExtensionOutputs.toJson() = + JSONObject( + mapOf( + "appidExclude" to appidExclude.getOrNull(), + "credProps" to credProps.getOrNull(), + "largeBlob" to largeBlob.getOrNull(), + ).filter { entry -> entry.value != null }, + ) + +private fun AuthenticatorAttestationResponse.toJson() = + JSONObject( + mapOf( + "attestationObject" to attestationObject.base64Url, + "clientData" to clientData.toJson(), + "clientDataJSON" to clientDataJSON.base64Url, + "transports" to transports.toJson(), + ).filter { entry -> entry.value != null }, + ) + +private fun CollectedClientData.toJson() = + JSONObject( + mapOf( + "challenge" to challenge.base64Url, + "origin" to origin, + "type" to type, + ).filter { entry -> entry.value != null }, + ) + +private fun SortedSet.toJson() = + JSONArray( + mapNotNull { + it.id + }, + ) diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/Extensions.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/Extensions.kt index 26b8804..eba3e29 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/Extensions.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/Extensions.kt @@ -5,8 +5,9 @@ package io.yubicolabs.wwwwallet * @return name for this class, or a placeholder. */ inline val Any?.tagForLog: String - get() = try { - this?.javaClass?.simpleName ?: "<|>" - } catch (th: Throwable) { - "<||>" - } + get() = + try { + this?.javaClass?.simpleName ?: "<|>" + } catch (th: Throwable) { + "<||>" + } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainActivity.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainActivity.kt index 71478d3..1e35b58 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainActivity.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainActivity.kt @@ -86,9 +86,11 @@ class MainActivity : ComponentActivity() { if (BuildConfig.DEBUG) { DebugMenuHandler( context = this, - showUrlRow = { vm.showUrlRow(it) } + showUrlRow = { vm.showUrlRow(it) }, ) - } else null + } else { + null + }, ) } @@ -97,14 +99,14 @@ class MainActivity : ComponentActivity() { vm.activity = this // 👀 (NFC) onBackPressedDispatcher.addCallback( - owner = this + owner = this, ) { vm.onBackPressed() } super.onCreate(savedInstanceState) setContent { MaterialTheme( - if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(), ) { val urlRow by vm.showUrlRow.collectAsState() @@ -123,18 +125,19 @@ class MainActivity : ComponentActivity() { }) { Icon( painter = painterResource(id = R.drawable.baseline_refresh_24), - contentDescription = null + contentDescription = null, ) } - } + }, ) } }, ) { paddingValues -> Column( - modifier = Modifier - .padding(paddingValues) - .fillMaxHeight() + modifier = + Modifier + .padding(paddingValues) + .fillMaxHeight(), ) { if (urlRow) { UrlRow(vm) @@ -147,7 +150,7 @@ class MainActivity : ComponentActivity() { javascriptInterfaceCreator = javascriptInterfaceCreator, javascriptInterfaceName = JAVASCRIPT_BRIDGE_NAME, vm.url.collectAsState().value ?: "", - vm::setUrl + vm::setUrl, ) } } @@ -175,19 +178,21 @@ fun ColumnScope.UrlRow(vm: MainViewModel) { onValueChange = { tempUrl = it }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Go, - ), - keyboardActions = KeyboardActions { - vm.setUrl(tempUrl) - keyboardController?.hide() - }, + keyboardOptions = + KeyboardOptions( + imeAction = ImeAction.Go, + ), + keyboardActions = + KeyboardActions { + vm.setUrl(tempUrl) + keyboardController?.hide() + }, ) IconButton(onClick = { vm.setUrl(tempUrl) }) { Icon( painter = painterResource(id = android.R.drawable.ic_menu_upload), - contentDescription = null + contentDescription = null, ) } } @@ -204,23 +209,25 @@ fun ColumnScope.WebView( setUrl: (String) -> Unit, ) { AndroidView( - modifier = Modifier.wrapContentHeight( - align = Alignment.Top, - ), - factory = createWebViewFactory( - activity = activity, - webViewClient = webViewClient, - webChromeClient = webChromeClient, - javascriptInterfaceCreator = javascriptInterfaceCreator, - javascriptInterfaceName = javascriptInterfaceName, - ), + modifier = + Modifier.wrapContentHeight( + align = Alignment.Top, + ), + factory = + createWebViewFactory( + activity = activity, + webViewClient = webViewClient, + webChromeClient = webChromeClient, + javascriptInterfaceCreator = javascriptInterfaceCreator, + javascriptInterfaceName = javascriptInterfaceName, + ), update = { webView: WebView -> updateWebView( webView = webView, url = url, - newUrlCallback = setUrl + newUrlCallback = setUrl, ) - } + }, ) } @@ -233,9 +240,10 @@ private fun createWebViewFactory( javascriptInterfaceCreator: (WebView) -> Any, javascriptInterfaceName: String, ) = { context: Context -> - val webView = WebView(activity).apply { - setNetworkAvailable(true) - } + val webView = + WebView(activity).apply { + setNetworkAvailable(true) + } webView.settings.apply { javaScriptEnabled = true @@ -258,13 +266,13 @@ private fun createWebViewFactory( override fun shouldInterceptRequest(request: WebResourceRequest?): WebResourceResponse? { return super.shouldInterceptRequest(request) } - } + }, ) } webView.addJavascriptInterface( javascriptInterfaceCreator(webView), - javascriptInterfaceName + javascriptInterfaceName, ) webView @@ -273,7 +281,7 @@ private fun createWebViewFactory( private fun updateWebView( webView: WebView, url: String?, - newUrlCallback: (String) -> Unit + newUrlCallback: (String) -> Unit, ) { if (url?.isNotBlank() == true) { if (url == "webview://back") { @@ -281,13 +289,14 @@ private fun updateWebView( """ window.history.back() document.location.href - """.trimIndent() + """.trimIndent(), ) { - val newUrl = if (it.contains("\"")) { - it.split("\"")[1] - } else { - it - } + val newUrl = + if (it.contains("\"")) { + it.split("\"")[1] + } else { + it + } Log.i(webView.tagForLog, "Reached $newUrl after back.") newUrlCallback("") @@ -295,9 +304,10 @@ private fun updateWebView( } else { webView.loadUrl(url) } - webView.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) + webView.layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) } } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainViewModel.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainViewModel.kt index 169fdd0..5944c8b 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainViewModel.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/MainViewModel.kt @@ -27,9 +27,9 @@ class MainViewModel : ViewModel() { _url.update { when { url.isBlank() or - url.startsWith("http://") or - url.startsWith("https://") - -> url + url.startsWith("http://") or + url.startsWith("https://") + -> url url.startsWith("openid4vp://") -> { url.replace("openid4vp://", BuildConfig.BASE_URL) diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleClientHandler.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleClientHandler.kt index 76f8352..8546981 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleClientHandler.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleClientHandler.kt @@ -2,7 +2,6 @@ package io.yubicolabs.wwwwallet.bluetooth - import android.app.Activity import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice @@ -23,7 +22,9 @@ import android.bluetooth.le.ScanSettings import android.content.pm.PackageManager import android.os.ParcelUuid import android.util.Log -import io.yubicolabs.wwwwallet.bluetooth.BleClientHandler.State.* +import io.yubicolabs.wwwwallet.bluetooth.BleClientHandler.State.Connected +import io.yubicolabs.wwwwallet.bluetooth.BleClientHandler.State.Disconnected +import io.yubicolabs.wwwwallet.bluetooth.BleClientHandler.State.Scanning import io.yubicolabs.wwwwallet.bluetooth.ServiceCharacteristic.Companion.ClientToServer import io.yubicolabs.wwwwallet.bluetooth.debug.PrintingBluetoothGattCallback import io.yubicolabs.wwwwallet.bluetooth.debug.PrintingScanCallback @@ -45,7 +46,6 @@ class BleClientHandler( val server: BluetoothGatt, val service: BluetoothGattService, val device: BluetoothDevice, - val readCallback: ((ByteArray?) -> Unit)? = null, val writeCallback: (() -> Unit)? = null, ) : State() @@ -64,250 +64,258 @@ class BleClientHandler( var state: State = Disconnected - val gattCallback = object : PrintingBluetoothGattCallback() { - override fun onConnectionStateChange( - gatt: BluetoothGatt?, - status: Int, - newState: Int - ) { - super.onConnectionStateChange(gatt, status, newState) - - if (gatt != null) { - if (newState == BluetoothProfile.STATE_CONNECTED) { - Log.d(tagForLog, "Connected") - try { - gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH) - gatt.requestMtu(517) - gatt.discoverServices() - - } catch (e: SecurityException) { - Log.e( - tagForLog, - "Couldn't connect to gatt", - e - ) + val gattCallback = + object : PrintingBluetoothGattCallback() { + override fun onConnectionStateChange( + gatt: BluetoothGatt?, + status: Int, + newState: Int, + ) { + super.onConnectionStateChange(gatt, status, newState) + + if (gatt != null) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.d(tagForLog, "Connected") + try { + gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH) + gatt.requestMtu(517) + gatt.discoverServices() + } catch (e: SecurityException) { + Log.e( + tagForLog, + "Couldn't connect to gatt", + e, + ) - if (state is Scanning) { - (state as Scanning).failureCallback() - } - } - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - state.let { - when (it) { - is Connected -> { - it.server.close() + if (state is Scanning) { + (state as Scanning).failureCallback() } + } + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + state.let { + when (it) { + is Connected -> { + it.server.close() + } - is Scanning -> { - it.scanner.stopScan(scanCallback) - } + is Scanning -> { + it.scanner.stopScan(scanCallback) + } - Disconnected -> { - // Already disconnected + Disconnected -> { + // Already disconnected + } } } - } - state = Disconnected - Log.d(tagForLog, "Disconnected") + state = Disconnected + Log.d(tagForLog, "Disconnected") + } } } - } - override fun onReliableWriteCompleted( - gatt: BluetoothGatt?, - status: Int - ) { - super.onReliableWriteCompleted(gatt, status) - } + override fun onReliableWriteCompleted( + gatt: BluetoothGatt?, + status: Int, + ) { + super.onReliableWriteCompleted(gatt, status) + } - override fun onServiceChanged(gatt: BluetoothGatt) { - super.onServiceChanged(gatt) - } + override fun onServiceChanged(gatt: BluetoothGatt) { + super.onServiceChanged(gatt) + } - override fun onCharacteristicChanged( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - value: ByteArray - ) { - Thread.sleep(100) // 👀 slow down communication - super.onCharacteristicChanged(gatt, characteristic, value) - - if (characteristic.uuid == ServiceCharacteristic.ServerToClient.uuid) { - if (state is Connected) { - (state as Connected).readCallback?.invoke(value) + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + ) { + Thread.sleep(100) // 👀 slow down communication + super.onCharacteristicChanged(gatt, characteristic, value) + + if (characteristic.uuid == ServiceCharacteristic.ServerToClient.uuid) { + if (state is Connected) { + (state as Connected).readCallback?.invoke(value) + } } } - } - override fun onCharacteristicRead( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - value: ByteArray, - status: Int - ) { - super.onCharacteristicRead( - gatt, - characteristic, - value, - status - ) + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + status: Int, + ) { + super.onCharacteristicRead( + gatt, + characteristic, + value, + status, + ) - state.let { - when (it) { - is Connected -> { - it.readCallback?.invoke(value) + state.let { + when (it) { + is Connected -> { + it.readCallback?.invoke(value) - state = it.copy( - readCallback = null - ) - } + state = + it.copy( + readCallback = null, + ) + } - is Disconnected -> Log.e(tagForLog, "Cannot read in disconnected state.") - is Scanning -> Log.e(tagForLog, "Trying to read while scanning.") + is Disconnected -> Log.e(tagForLog, "Cannot read in disconnected state.") + is Scanning -> Log.e(tagForLog, "Trying to read while scanning.") + } } } - } - override fun onCharacteristicWrite( - gatt: BluetoothGatt?, - characteristic: BluetoothGattCharacteristic?, - status: Int - ) { - Thread.sleep(100) // 👀 slow down communication - super.onCharacteristicWrite(gatt, characteristic, status) - - state.let { - when (it) { - is Connected -> { - when (characteristic?.uuid) { - ServiceCharacteristic.ServerToClient.uuid -> { - it.writeCallback?.invoke() - state = it.copy( - writeCallback = null - ) - } + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + status: Int, + ) { + Thread.sleep(100) // 👀 slow down communication + super.onCharacteristicWrite(gatt, characteristic, status) - ServiceCharacteristic.ClientToServer.uuid -> { - it.writeCallback?.invoke() - state = it.copy( - writeCallback = null - ) - } + state.let { + when (it) { + is Connected -> { + when (characteristic?.uuid) { + ServiceCharacteristic.ServerToClient.uuid -> { + it.writeCallback?.invoke() + state = + it.copy( + writeCallback = null, + ) + } - ServiceCharacteristic.State.uuid -> { - it.writeCallback?.invoke() - state = it.copy( - writeCallback = null - ) - } + ServiceCharacteristic.ClientToServer.uuid -> { + it.writeCallback?.invoke() + state = + it.copy( + writeCallback = null, + ) + } - else -> Log.e( - tagForLog, - "Cannot write to UUID '${characteristic?.uuid}'." - ) + ServiceCharacteristic.State.uuid -> { + it.writeCallback?.invoke() + state = + it.copy( + writeCallback = null, + ) + } + + else -> + Log.e( + tagForLog, + "Cannot write to UUID '${characteristic?.uuid}'.", + ) + } } - } - is Disconnected -> Log.e(tagForLog, "Cannot write in disconnected state.") - is Scanning -> Log.e(tagForLog, "Trying to write while scanning.") + is Disconnected -> Log.e(tagForLog, "Cannot write in disconnected state.") + is Scanning -> Log.e(tagForLog, "Trying to write while scanning.") + } } } - } - override fun onDescriptorWrite( - gatt: BluetoothGatt?, - descriptor: BluetoothGattDescriptor?, - status: Int - ) { - super.onDescriptorWrite(gatt, descriptor, status) - if (descriptor?.uuid == ServiceCharacteristic.CharacteristicConfigurationDescriptorUUID) { - val characteristic = descriptor.characteristic - gatt?.setCharacteristicNotification(characteristic, true) + override fun onDescriptorWrite( + gatt: BluetoothGatt?, + descriptor: BluetoothGattDescriptor?, + status: Int, + ) { + super.onDescriptorWrite(gatt, descriptor, status) + if (descriptor?.uuid == ServiceCharacteristic.CharacteristicConfigurationDescriptorUUID) { + val characteristic = descriptor.characteristic + gatt?.setCharacteristicNotification(characteristic, true) + } } - } - override fun onServicesDiscovered( - gatt: BluetoothGatt?, - status: Int - ) { - super.onServicesDiscovered(gatt, status) + override fun onServicesDiscovered( + gatt: BluetoothGatt?, + status: Int, + ) { + super.onServicesDiscovered(gatt, status) - if (status == GATT_SUCCESS && gatt != null) { - state.let { - when (it) { - is Scanning -> { - val service = gatt.getService(it.serviceUuid) - if (service != null) { - val char = service - .getCharacteristic(ServiceCharacteristic.ServerToClient.uuid) - if (char == null) { + if (status == GATT_SUCCESS && gatt != null) { + state.let { + when (it) { + is Scanning -> { + val service = gatt.getService(it.serviceUuid) + if (service != null) { + val char = + service + .getCharacteristic(ServiceCharacteristic.ServerToClient.uuid) + if (char == null) { + Log.e( + tagForLog, + "ServerToClient (${ServiceCharacteristic.ServerToClient.uuid}) not found.", + ) + } else { + gatt.setCharacteristicNotification(char, true) + } + + state = + Connected( + gatt, + service, + gatt.device, + null, + ) + + val bleStateChar = + service.getCharacteristic( + ServiceCharacteristic.State.uuid, + ) + gatt.writeCharacteristic( + bleStateChar, + byteArrayOf(0x1), + WRITE_TYPE_NO_RESPONSE, + ) + + it.successCallback() + } else { Log.e( tagForLog, - "ServerToClient (${ServiceCharacteristic.ServerToClient.uuid}) not found." + "Service with ${it.serviceUuid} not found.", ) - } else { - gatt.setCharacteristicNotification(char, true) + it.failureCallback() } - - state = Connected( - gatt, - service, - gatt.device, - null - ) - - val bleStateChar = service.getCharacteristic( - ServiceCharacteristic.State.uuid - ) - gatt.writeCharacteristic( - bleStateChar, - byteArrayOf(0x1), - WRITE_TYPE_NO_RESPONSE - ) - - it.successCallback() - } else { - Log.e( - tagForLog, - "Service with ${it.serviceUuid} not found." - ) - it.failureCallback() } - } - else -> Log.e(tagForLog, "Discovered a service while not scanning.") + else -> Log.e(tagForLog, "Discovered a service while not scanning.") + } } } } } - } - val scanCallback: ScanCallback = object : PrintingScanCallback() { - override fun onScanResult( - callbackType: Int, - result: ScanResult? - ) { - super.onScanResult(callbackType, result) - state.let { - if (it !is Scanning) { - Log.e(tagForLog, "Scanning stopped while not in scanning state.") - } else { - - it.scanner.stopScan(scanCallback) - - result - ?.device - ?.connectGatt( - activity, - false, - gattCallback, - BluetoothDevice.TRANSPORT_LE - ) + val scanCallback: ScanCallback = + object : PrintingScanCallback() { + override fun onScanResult( + callbackType: Int, + result: ScanResult?, + ) { + super.onScanResult(callbackType, result) + state.let { + if (it !is Scanning) { + Log.e(tagForLog, "Scanning stopped while not in scanning state.") + } else { + it.scanner.stopScan(scanCallback) + + result + ?.device + ?.connectGatt( + activity, + false, + gattCallback, + BluetoothDevice.TRANSPORT_LE, + ) + } } } } - } fun status(): String { return state.let { @@ -337,35 +345,38 @@ class BleClientHandler( } val serviceUuid = UUID.fromString(serviceUuid) - val settings = ScanSettings - .Builder() - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build() + val settings = + ScanSettings + .Builder() + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() val scanner = adapter.bluetoothLeScanner - state = Scanning( - serviceUuid, - scanner, - success, - failure - ) - - val filterList = listOf( - ScanFilter - .Builder() - .setServiceUuid( - ParcelUuid( - serviceUuid - ) - ).build() - ) + state = + Scanning( + serviceUuid, + scanner, + success, + failure, + ) + + val filterList = + listOf( + ScanFilter + .Builder() + .setServiceUuid( + ParcelUuid( + serviceUuid, + ), + ).build(), + ) try { scanner!!.startScan( filterList, settings, - scanCallback + scanCallback, ) } catch (e: SecurityException) { Log.e(tagForLog, "Couldn't start scanning.", e) @@ -402,18 +413,19 @@ class BleClientHandler( it.server.writeCharacteristic( characteristic, payload, - WRITE_TYPE_DEFAULT + WRITE_TYPE_DEFAULT, ) - state = it.copy( - writeCallback = success - ) + state = + it.copy( + writeCallback = success, + ) } else -> { Log.e( tagForLog, - "Cannot send in state ${it.javaClass.simpleName} to server." + "Cannot send in state ${it.javaClass.simpleName} to server.", ) failure() } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleServerHandler.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleServerHandler.kt index a7c82a0..979e921 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleServerHandler.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/BleServerHandler.kt @@ -2,7 +2,6 @@ package io.yubicolabs.wwwwallet.bluetooth - import android.app.Activity import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice @@ -22,7 +21,9 @@ import android.bluetooth.le.BluetoothLeAdvertiser import android.content.pm.PackageManager import android.os.ParcelUuid import android.util.Log -import io.yubicolabs.wwwwallet.bluetooth.BleServerHandler.State.* +import io.yubicolabs.wwwwallet.bluetooth.BleServerHandler.State.Advertising +import io.yubicolabs.wwwwallet.bluetooth.BleServerHandler.State.Connected +import io.yubicolabs.wwwwallet.bluetooth.BleServerHandler.State.Disconnected import io.yubicolabs.wwwwallet.bluetooth.ServiceCharacteristic.Companion.ServerToClient import io.yubicolabs.wwwwallet.bluetooth.debug.PrintingAdvertiseCallback import io.yubicolabs.wwwwallet.bluetooth.debug.PrintingBluetoothGattServerCallback @@ -36,14 +37,13 @@ class BleServerHandler( data class Advertising( val server: BluetoothGattServer, val service: BluetoothGattService, - val advertiser: BluetoothLeAdvertiser + val advertiser: BluetoothLeAdvertiser, ) : State() data class Connected( val server: BluetoothGattServer, val service: BluetoothGattService, val device: BluetoothDevice, - val readCallback: ((ByteArray?) -> Unit)? = null, val writeCallback: (() -> Unit)? = null, ) : State() @@ -74,7 +74,7 @@ class BleServerHandler( listen( serviceUuid, success, - failure + failure, ) } } @@ -82,7 +82,7 @@ class BleServerHandler( private fun listen( rawServiceUuid: String, success: () -> Unit, - failure: () -> Unit + failure: () -> Unit, ) { val serviceUuid = UUID.fromString(rawServiceUuid) @@ -90,7 +90,7 @@ class BleServerHandler( gattServer = manager.openGattServer( activity, - callback + callback, ) if (gattServer == null) { @@ -99,10 +99,11 @@ class BleServerHandler( return } - val service = BluetoothGattService( - serviceUuid, - SERVICE_TYPE_PRIMARY - ) + val service = + BluetoothGattService( + serviceUuid, + SERVICE_TYPE_PRIMARY, + ) ServiceCharacteristic.toBleCharacteristics().map { characteristic -> service.addCharacteristic(characteristic) @@ -115,28 +116,30 @@ class BleServerHandler( } val advertiser = adapter!!.bluetoothLeAdvertiser - val settings = AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) - .setConnectable(true) - .setTimeout(0) - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .build() - - val data = AdvertiseData.Builder() - .setIncludeTxPowerLevel(false) - .addServiceUuid(ParcelUuid(serviceUuid)) - .build() + val settings = + AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .setConnectable(true) + .setTimeout(0) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) + .build() + + val data = + AdvertiseData.Builder() + .setIncludeTxPowerLevel(false) + .addServiceUuid(ParcelUuid(serviceUuid)) + .build() Log.d( tagForLog, - "Started advertising UUID $serviceUuid as advertiser $advertiser on gattserver $gattServer." + "Started advertising UUID $serviceUuid as advertiser $advertiser on gattserver $gattServer.", ) try { advertiser.startAdvertising( settings, data, - printingAdvertiserCallback + printingAdvertiserCallback, ) state = Advertising(gattServer, service, advertiser) @@ -175,7 +178,6 @@ class BleServerHandler( } else -> { - } } } @@ -221,165 +223,167 @@ class BleServerHandler( } } - private val callback = object : PrintingBluetoothGattServerCallback() { - override fun onConnectionStateChange( - device: BluetoothDevice?, - status: Int, - newState: Int - ) { - super.onConnectionStateChange(device, status, newState) - - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - state.let { - if (device != null && it is Advertising) { - it.advertiser.stopAdvertising(printingAdvertiserCallback) - - state = Connected( - it.server, - it.service, - device, - null - ) + private val callback = + object : PrintingBluetoothGattServerCallback() { + override fun onConnectionStateChange( + device: BluetoothDevice?, + status: Int, + newState: Int, + ) { + super.onConnectionStateChange(device, status, newState) + + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + state.let { + if (device != null && it is Advertising) { + it.advertiser.stopAdvertising(printingAdvertiserCallback) + + state = + Connected( + it.server, + it.service, + device, + null, + ) + } } } - } - BluetoothProfile.STATE_DISCONNECTED -> { - state.let { - if (it is Advertising) { - it.advertiser.stopAdvertising( - PrintingAdvertiseCallback() - ) - } + BluetoothProfile.STATE_DISCONNECTED -> { + state.let { + if (it is Advertising) { + it.advertiser.stopAdvertising( + PrintingAdvertiseCallback(), + ) + } - state = Disconnected + state = Disconnected + } } } } - } - - override fun onDescriptorWriteRequest( - device: BluetoothDevice?, - requestId: Int, - descriptor: BluetoothGattDescriptor?, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - value: ByteArray? - ) { - super.onDescriptorWriteRequest( - device, - requestId, - descriptor, - preparedWrite, - responseNeeded, - offset, - value - ) - } - override fun onCharacteristicWriteRequest( - device: BluetoothDevice?, - requestId: Int, - characteristic: BluetoothGattCharacteristic?, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - payload: ByteArray? - ) { - super.onCharacteristicWriteRequest( - device, - requestId, - characteristic, - preparedWrite, - responseNeeded, - offset, - payload - ) + override fun onDescriptorWriteRequest( + device: BluetoothDevice?, + requestId: Int, + descriptor: BluetoothGattDescriptor?, + preparedWrite: Boolean, + responseNeeded: Boolean, + offset: Int, + value: ByteArray?, + ) { + super.onDescriptorWriteRequest( + device, + requestId, + descriptor, + preparedWrite, + responseNeeded, + offset, + value, + ) + } - state.let { - if (it is Connected) { - val writeState = if (payload != null) { - when (characteristic?.uuid) { - ServiceCharacteristic.ClientToServer.uuid -> { - // registered characteristic found, report back - val msg = - "Received $payload (${payload.toHumanReadable()}, ${ - String( - payload + override fun onCharacteristicWriteRequest( + device: BluetoothDevice?, + requestId: Int, + characteristic: BluetoothGattCharacteristic?, + preparedWrite: Boolean, + responseNeeded: Boolean, + offset: Int, + payload: ByteArray?, + ) { + super.onCharacteristicWriteRequest( + device, + requestId, + characteristic, + preparedWrite, + responseNeeded, + offset, + payload, + ) + + state.let { + if (it is Connected) { + val writeState = + if (payload != null) { + when (characteristic?.uuid) { + ServiceCharacteristic.ClientToServer.uuid -> { + // registered characteristic found, report back + val msg = + "Received $payload (${payload.toHumanReadable()}, ${ + String( + payload, + ) + }) from ${characteristic.uuid}" + Log.d(tagForLog, msg) + + it.server.notifyCharacteristicChanged( + device!!, + characteristic, + false, + payload, ) - }) from ${characteristic.uuid}" - Log.d(tagForLog, msg) - - it.server.notifyCharacteristicChanged( - device!!, - characteristic, - false, - payload - ) - - // check if server wanted to see what client wrote. - if (it.readCallback != null) { - it.readCallback(payload) - state = it.copy(readCallback = null) - } - GATT_SUCCESS - } + // check if server wanted to see what client wrote. + if (it.readCallback != null) { + it.readCallback(payload) + state = it.copy(readCallback = null) + } - ServerToClient.uuid -> { + GATT_SUCCESS + } - GATT_SUCCESS - } + ServerToClient.uuid -> { + GATT_SUCCESS + } - else -> { - Log.e( - tagForLog, - "Unexpected characteristic ($characteristic) write received." - ) + else -> { + Log.e( + tagForLog, + "Unexpected characteristic ($characteristic) write received.", + ) + GATT_FAILURE + } + } + } else { + Log.e(tagForLog, "Received empty write request payload.") GATT_FAILURE } + + if (responseNeeded) { + it.server.sendResponse( + device, + requestId, + writeState, + offset, + payload, + ) } } else { - Log.e(tagForLog, "Received empty write request payload.") - GATT_FAILURE + Log.e(tagForLog, "Cannot write in $state.") } - - if (responseNeeded) { - it.server.sendResponse( - device, - requestId, - writeState, - offset, - payload - ) - } - } else { - Log.e(tagForLog, "Cannot write in $state.") } } - } - override fun onCharacteristicReadRequest( - device: BluetoothDevice?, - requestId: Int, - offset: Int, - characteristic: BluetoothGattCharacteristic? - ) { - super.onCharacteristicReadRequest(device, requestId, offset, characteristic) - - state.let { - if (it is Connected) { - if (characteristic?.uuid == ServerToClient) { - if (it.readCallback != null) { - it.readCallback(characteristic.value) + override fun onCharacteristicReadRequest( + device: BluetoothDevice?, + requestId: Int, + offset: Int, + characteristic: BluetoothGattCharacteristic?, + ) { + super.onCharacteristicReadRequest(device, requestId, offset, characteristic) + + state.let { + if (it is Connected) { + if (characteristic?.uuid == ServerToClient) { + if (it.readCallback != null) { + it.readCallback(characteristic.value) + } } } } } } - } private val printingAdvertiserCallback = PrintingAdvertiseCallback() } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/Extensions.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/Extensions.kt index 7be8187..5b5d0a3 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/Extensions.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/Extensions.kt @@ -10,15 +10,19 @@ import android.content.Intent import android.content.pm.PackageManager.PERMISSION_DENIED import android.util.Log -fun ByteArray?.toHumanReadable(): String = if (this == null) { - byteArrayOf() -} else { - this -}.joinToString(separator = " ") { b -> - "0x%02x".format(b) -} +fun ByteArray?.toHumanReadable(): String = + if (this == null) { + byteArrayOf() + } else { + this + }.joinToString(separator = " ") { b -> + "0x%02x".format(b) + } -fun checkBluetoothPermissions(activity: Activity, adapter: BluetoothAdapter?): Boolean { +fun checkBluetoothPermissions( + activity: Activity, + adapter: BluetoothAdapter?, +): Boolean { if (adapter == null) { Log.e("BLEPERM", "No bluetooth device.") return false diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/ServiceCharacteristics.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/ServiceCharacteristics.kt index d2e5ab4..109f795 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/ServiceCharacteristics.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/ServiceCharacteristics.kt @@ -4,11 +4,9 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import java.util.UUID - sealed class ServiceCharacteristic( val name: String, val uuid: UUID, - val broadcastProperty: Boolean = false, val readProperty: Boolean = false, val writeNoResponseProperty: Boolean = false, @@ -16,7 +14,6 @@ sealed class ServiceCharacteristic( val notifyProperty: Boolean = false, val indicateProperty: Boolean = false, val signedWriteProperty: Boolean = false, - val readPermission: Boolean = false, val readEncryptedPermission: Boolean = false, val readEncryptedMitmPermission: Boolean = false, @@ -35,54 +32,62 @@ sealed class ServiceCharacteristic( private fun all(): List = when (mode) { - Mode.MDoc -> listOf( - MDoc.State, - MDoc.ServerToClient, - MDoc.ClientToServer - ) - - Mode.MDocReader -> listOf( - MDocReader.State, - MDocReader.ServerToClient, - MDocReader.ClientToServer - ) + Mode.MDoc -> + listOf( + MDoc.State, + MDoc.ServerToClient, + MDoc.ClientToServer, + ) + + Mode.MDocReader -> + listOf( + MDocReader.State, + MDocReader.ServerToClient, + MDocReader.ClientToServer, + ) } val ClientToServer: ServiceCharacteristic - get() = when (mode) { - Mode.MDoc -> MDoc.ClientToServer - Mode.MDocReader -> MDocReader.ClientToServer - } + get() = + when (mode) { + Mode.MDoc -> MDoc.ClientToServer + Mode.MDocReader -> MDocReader.ClientToServer + } val ServerToClient: ServiceCharacteristic - get() = when (mode) { - Mode.MDoc -> MDoc.ServerToClient - Mode.MDocReader -> MDocReader.ServerToClient - } + get() = + when (mode) { + Mode.MDoc -> MDoc.ServerToClient + Mode.MDocReader -> MDocReader.ServerToClient + } val State: ServiceCharacteristic - get() = when (mode) { - Mode.MDoc -> MDoc.State - Mode.MDocReader -> MDocReader.State - } - - fun toBleCharacteristics(): List = all().map { characteristic -> - val bleCharacteristic = BluetoothGattCharacteristic( - characteristic.uuid, - characteristic.bleProperties, - characteristic.blePermissions, - ) - if (characteristic.isASpecialSnowflake) { - val descriptor = BluetoothGattDescriptor( - CharacteristicConfigurationDescriptorUUID, - BluetoothGattDescriptor.PERMISSION_WRITE - ) - descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) - bleCharacteristic.addDescriptor(descriptor) + get() = + when (mode) { + Mode.MDoc -> MDoc.State + Mode.MDocReader -> MDocReader.State + } + + fun toBleCharacteristics(): List = + all().map { characteristic -> + val bleCharacteristic = + BluetoothGattCharacteristic( + characteristic.uuid, + characteristic.bleProperties, + characteristic.blePermissions, + ) + if (characteristic.isASpecialSnowflake) { + val descriptor = + BluetoothGattDescriptor( + CharacteristicConfigurationDescriptorUUID, + BluetoothGattDescriptor.PERMISSION_WRITE, + ) + descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) + bleCharacteristic.addDescriptor(descriptor) + } + + bleCharacteristic } - - bleCharacteristic - } } private data object MDoc { @@ -143,12 +148,13 @@ sealed class ServiceCharacteristic( enum class Mode { MDocReader, - MDoc + MDoc, } } val ServiceCharacteristic.bleProperties: Int - get() = (if (broadcastProperty) BluetoothGattCharacteristic.PROPERTY_BROADCAST else 0) or + get() = + (if (broadcastProperty) BluetoothGattCharacteristic.PROPERTY_BROADCAST else 0) or (if (readProperty) BluetoothGattCharacteristic.PROPERTY_READ else 0) or (if (writeNoResponseProperty) BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE else 0) or (if (writeProperty) BluetoothGattCharacteristic.PROPERTY_WRITE else 0) or @@ -157,7 +163,8 @@ val ServiceCharacteristic.bleProperties: Int (if (signedWriteProperty) BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE else 0) val ServiceCharacteristic.blePermissions: Int - get() = (if (readPermission) BluetoothGattCharacteristic.PERMISSION_READ else 0) or + get() = + (if (readPermission) BluetoothGattCharacteristic.PERMISSION_READ else 0) or (if (readEncryptedPermission) BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED else 0) or (if (readEncryptedMitmPermission) BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM else 0) or (if (writePermission) BluetoothGattCharacteristic.PERMISSION_WRITE else 0) or diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattCallback.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattCallback.kt index 30939a5..0741d2e 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattCallback.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattCallback.kt @@ -14,7 +14,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onPhyUpdate: $gatt, $txPhy, $rxPhy, $status") super.onPhyUpdate(gatt, txPhy, rxPhy, status) @@ -24,7 +24,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onPhyRead: $gatt, $txPhy, $rxPhy, $status") super.onPhyRead(gatt, txPhy, rxPhy, status) @@ -33,13 +33,16 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onConnectionStateChange( gatt: BluetoothGatt?, status: Int, - newState: Int + newState: Int, ) { Log.d(tagForLog, "onConnectionStateChange: $gatt, $status, $newState") super.onConnectionStateChange(gatt, status, newState) } - override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + override fun onServicesDiscovered( + gatt: BluetoothGatt?, + status: Int, + ) { Log.d(tagForLog, "onServicesDiscovered: $gatt, $status") super.onServicesDiscovered(gatt, status) } @@ -48,7 +51,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onCharacteristicRead( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, - status: Int + status: Int, ) { Log.d(tagForLog, "onCharacteristicRead: $gatt, $characteristic, $status") super.onCharacteristicRead(gatt, characteristic, status) @@ -58,7 +61,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, - status: Int + status: Int, ) { Log.d(tagForLog, "onCharacteristicRead: $gatt, $characteristic, $value, $status") super.onCharacteristicRead(gatt, characteristic, value, status) @@ -67,11 +70,11 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onCharacteristicWrite( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, - status: Int + status: Int, ) { Log.d( tagForLog, - "onCharacteristicWrite: $gatt, $characteristic(${characteristic?.value}), $status" + "onCharacteristicWrite: $gatt, $characteristic(${characteristic?.value}), $status", ) super.onCharacteristicWrite(gatt, characteristic, status) } @@ -79,7 +82,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { @Deprecated("") override fun onCharacteristicChanged( gatt: BluetoothGatt?, - characteristic: BluetoothGattCharacteristic? + characteristic: BluetoothGattCharacteristic?, ) { Log.d(tagForLog, "onCharacteristicChanged: $gatt, $characteristic") super.onCharacteristicChanged(gatt, characteristic) @@ -88,7 +91,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, - value: ByteArray + value: ByteArray, ) { Log.d(tagForLog, "onCharacteristicChanged: $gatt, $characteristic, $value") super.onCharacteristicChanged(gatt, characteristic, value) @@ -98,7 +101,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onDescriptorRead( gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, - status: Int + status: Int, ) { Log.d(tagForLog, "onDescriptorRead: $gatt, $descriptor, $status") super.onDescriptorRead(gatt, descriptor, status) @@ -108,7 +111,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, - value: ByteArray + value: ByteArray, ) { Log.d(tagForLog, "onDescriptorRead: $gatt, $descriptor, $status, $value") super.onDescriptorRead(gatt, descriptor, status, value) @@ -117,7 +120,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onDescriptorWrite( gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, - status: Int + status: Int, ) { Log.d(tagForLog, "onDescriptorWrite: $gatt, $descriptor, $status") super.onDescriptorWrite(gatt, descriptor, status) @@ -125,7 +128,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onReliableWriteCompleted( gatt: BluetoothGatt?, - status: Int + status: Int, ) { Log.d(tagForLog, "onReliableWriteCompleted: $gatt, $status") super.onReliableWriteCompleted(gatt, status) @@ -134,7 +137,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onReadRemoteRssi( gatt: BluetoothGatt?, rssi: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onReadRemoteRssi: $gatt, $rssi, $status") super.onReadRemoteRssi(gatt, rssi, status) @@ -143,7 +146,7 @@ open class PrintingBluetoothGattCallback : BluetoothGattCallback() { override fun onMtuChanged( gatt: BluetoothGatt?, mtu: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onMtuChanged: $gatt, $mtu, $status") super.onMtuChanged(gatt, mtu, status) diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattServerCallback.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattServerCallback.kt index aed9dab..f65890e 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattServerCallback.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingBluetoothGattServerCallback.kt @@ -17,7 +17,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() override fun onConnectionStateChange( device: BluetoothDevice?, status: Int, - newState: Int + newState: Int, ) { Log.d(tagForLog, "onConnectionStateChange: $device, ${status.human}, ${newState.human}") @@ -26,7 +26,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() override fun onServiceAdded( status: Int, - service: BluetoothGattService? + service: BluetoothGattService?, ) { Log.d(tagForLog, "onServiceAdded: $status, $service") @@ -37,11 +37,11 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() device: BluetoothDevice?, requestId: Int, offset: Int, - characteristic: BluetoothGattCharacteristic? + characteristic: BluetoothGattCharacteristic?, ) { Log.d( tagForLog, - "onCharacteristicReadRequest: $device, $requestId, $offset, $characteristic" + "onCharacteristicReadRequest: $device, $requestId, $offset, $characteristic", ) super.onCharacteristicReadRequest(device, requestId, offset, characteristic) @@ -54,11 +54,11 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, - value: ByteArray? + value: ByteArray?, ) { Log.d( tagForLog, - "onCharacteristicWriteRequest: $device, $requestId, $characteristic, $preparedWrite, $responseNeeded, $offset, ${value.toHumanReadable()}" + "onCharacteristicWriteRequest: $device, $requestId, $characteristic, $preparedWrite, $responseNeeded, $offset, ${value.toHumanReadable()}", ) super.onCharacteristicWriteRequest( @@ -68,7 +68,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() preparedWrite, responseNeeded, offset, - value + value, ) } @@ -76,7 +76,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() device: BluetoothDevice?, requestId: Int, offset: Int, - descriptor: BluetoothGattDescriptor? + descriptor: BluetoothGattDescriptor?, ) { Log.d(tagForLog, "onDescriptorReadRequest: $device, $requestId, $offset, $descriptor") @@ -90,11 +90,11 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, - value: ByteArray? + value: ByteArray?, ) { Log.d( tagForLog, - "onDescriptorWriteRequest: $device, $requestId, $descriptor, $preparedWrite, $responseNeeded, $offset, ${value.toHumanReadable()}" + "onDescriptorWriteRequest: $device, $requestId, $descriptor, $preparedWrite, $responseNeeded, $offset, ${value.toHumanReadable()}", ) super.onDescriptorWriteRequest( @@ -104,14 +104,14 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() preparedWrite, responseNeeded, offset, - value + value, ) } override fun onExecuteWrite( device: BluetoothDevice?, requestId: Int, - execute: Boolean + execute: Boolean, ) { Log.d(tagForLog, "onExecuteWrite: $device, $requestId, $execute") @@ -120,14 +120,17 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() override fun onNotificationSent( device: BluetoothDevice?, - status: Int + status: Int, ) { Log.d(tagForLog, "onNotificationSent: $device, $status") super.onNotificationSent(device, status) } - override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { + override fun onMtuChanged( + device: BluetoothDevice?, + mtu: Int, + ) { Log.d(tagForLog, "onMtuChanged: $device, $mtu") super.onMtuChanged(device, mtu) @@ -137,7 +140,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() device: BluetoothDevice?, txPhy: Int, rxPhy: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onPhyUpdate: $device, $txPhy, $rxPhy, $status") @@ -148,7 +151,7 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() device: BluetoothDevice?, txPhy: Int, rxPhy: Int, - status: Int + status: Int, ) { Log.d(tagForLog, "onPhyRead $device, $txPhy, $rxPhy, $status") @@ -157,10 +160,11 @@ open class PrintingBluetoothGattServerCallback() : BluetoothGattServerCallback() } private val Int.human: String - get() = when (this) { - STATE_DISCONNECTED -> "STATE_DISCONNECTED" - STATE_CONNECTING -> "STATE_CONNECTING" - STATE_CONNECTED -> "STATE_CONNECTED" - STATE_DISCONNECTING -> "STATE_DISCONNECTING" - else -> "unknown" - } + get() = + when (this) { + STATE_DISCONNECTED -> "STATE_DISCONNECTED" + STATE_CONNECTING -> "STATE_CONNECTING" + STATE_CONNECTED -> "STATE_CONNECTED" + STATE_DISCONNECTING -> "STATE_DISCONNECTING" + else -> "unknown" + } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingScanCallback.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingScanCallback.kt index 85eaccd..4c3a094 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingScanCallback.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bluetooth/debug/PrintingScanCallback.kt @@ -6,7 +6,10 @@ import android.util.Log import io.yubicolabs.wwwwallet.tagForLog open class PrintingScanCallback : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult?) { + override fun onScanResult( + callbackType: Int, + result: ScanResult?, + ) { Log.d(tagForLog, "onScanResult: callbackType=$callbackType result=$result") super.onScanResult(callbackType, result) diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippet.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippet.kt index ac851e6..96d0728 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippet.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippet.kt @@ -12,16 +12,17 @@ class JSCodeSnippet( fun fromRawResource( context: Context, resource: String, - replacements: List> - ): JSCodeSnippet = JSCodeSnippet( - String( - context - .assets - .open(resource) - .readAllBytes() - ), - replacements - ) + replacements: List>, + ): JSCodeSnippet = + JSCodeSnippet( + String( + context + .assets + .open(resource) + .readAllBytes(), + ), + replacements, + ) } } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/WalletJsBridge.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/WalletJsBridge.kt index 5601703..fc7e452 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/WalletJsBridge.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/bridging/WalletJsBridge.kt @@ -27,7 +27,7 @@ class WalletJsBridge( private val emulatedCredentialsContainer: NavigatorCredentialsContainer, private val bleClientHandler: BleClientHandler, private val bleServerHandler: BleServerHandler, - private val debugMenuHandler: DebugMenuHandler? + private val debugMenuHandler: DebugMenuHandler?, ) { companion object { const val JAVASCRIPT_BRIDGE_NAME = "nativeWrapper" @@ -42,18 +42,19 @@ class WalletJsBridge( var selectedContainer: NavigatorCredentialsContainer? = null for (hint in hints) { - selectedContainer = when (hint) { - "security-key" -> securityKeyCredentialsContainer - "client-device" -> clientDeviceCredentialsContainer - "hybrid" -> null // explicitly not supported - // not in spec: added for testing - "emulator" -> emulatedCredentialsContainer - else -> { - // error case: unknown hint. - Log.e(tagForLog, "Hint '$hint' not supported. Ignoring.") - null + selectedContainer = + when (hint) { + "security-key" -> securityKeyCredentialsContainer + "client-device" -> clientDeviceCredentialsContainer + "hybrid" -> null // explicitly not supported + // not in spec: added for testing + "emulator" -> emulatedCredentialsContainer + else -> { + // error case: unknown hint. + Log.e(tagForLog, "Hint '$hint' not supported. Ignoring.") + null + } } - } if (selectedContainer != null) { break @@ -65,12 +66,11 @@ class WalletJsBridge( Log.i( tagForLog, "'hints' field in credential options not found, defaulting back to 'security-key'.", - jsonException + jsonException, ) securityKeyCredentialsContainer } - /** * Call this to overwrite the `navigator.credentials.[get|create]` methods. */ @@ -79,16 +79,20 @@ class WalletJsBridge( fun inject() { Log.i( tagForLog, - "Adding `${javaClass.simpleName}` as `$JAVASCRIPT_BRIDGE_NAME` to JS." + "Adding `${javaClass.simpleName}` as `$JAVASCRIPT_BRIDGE_NAME` to JS.", ) dispatcher.dispatch(EmptyCoroutineContext) { - val injectionSnippet = JSCodeSnippet.fromRawResource( - context = webView.context, resource = "injectjs.js", replacements = listOf( - "JAVASCRIPT_BRIDGE" to JAVASCRIPT_BRIDGE_NAME, - "JAVASCRIPT_VISUALIZE_INJECTION" to "${BuildConfig.VISUALIZE_INJECTION}" + val injectionSnippet = + JSCodeSnippet.fromRawResource( + context = webView.context, + resource = "injectjs.js", + replacements = + listOf( + "JAVASCRIPT_BRIDGE" to JAVASCRIPT_BRIDGE_NAME, + "JAVASCRIPT_VISUALIZE_INJECTION" to "${BuildConfig.VISUALIZE_INJECTION}", + ), ) - ) webView.evaluateJavascript(injectionSnippet.code) { Log.i(it.tagForLog, it) @@ -115,7 +119,7 @@ class WalletJsBridge( @SuppressLint("unused") fun create( promiseUuid: String, - options: String + options: String, ) { val mappedOptions = JSONObject(options) mappedOptions.setNested("publicKey.attestation", "none") @@ -133,7 +137,7 @@ class WalletJsBridge( console.log('credential creation failed', JSON.stringify("$th")) alert('Credential creation failed: ' + JSON.stringify("${th.localizedMessage}")) $JAVASCRIPT_BRIDGE_NAME.__reject__("$promiseUuid", JSON.stringify("$th")); - """.trimIndent() + """.trimIndent(), ) {} } }, @@ -146,10 +150,10 @@ class WalletJsBridge( var response = JSON.parse('$response') console.log('credential created', response) $JAVASCRIPT_BRIDGE_NAME.__resolve__("$promiseUuid", response); - """.trimIndent() + """.trimIndent(), ) {} } - } + }, ) } @@ -157,7 +161,7 @@ class WalletJsBridge( @SuppressLint("unused") fun get( promiseUuid: String, - options: String + options: String, ) { Log.i(tagForLog, "$JAVASCRIPT_BRIDGE_NAME.get($promiseUuid, $options) called.") @@ -174,7 +178,7 @@ class WalletJsBridge( console.log('credential getting failed', JSON.stringify("$th")) alert('Credential getting failed: ' + JSON.stringify("${th.localizedMessage}")) $JAVASCRIPT_BRIDGE_NAME.__reject__("$promiseUuid", JSON.stringify("$th")); - """.trimIndent() + """.trimIndent(), ) {} } }, @@ -187,10 +191,10 @@ class WalletJsBridge( var response = JSON.parse('$response') console.log('credential getted', response) $JAVASCRIPT_BRIDGE_NAME.__resolve__("$promiseUuid", response); - """.trimIndent() + """.trimIndent(), ) {} } - } + }, ) } @@ -198,14 +202,14 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothStatusWrapped( promiseUuid: String, - unusedParameter: String + unusedParameter: String, ) { resolvePromise( promiseUuid, // @formatter:off "Mode: ${ServiceCharacteristic.mode.name}\\n\\n" + - "Server: ${bleServerHandler.status()}\\n\\n" + - "Client: ${bleClientHandler.status()}" + "Server: ${bleServerHandler.status()}\\n\\n" + + "Client: ${bleClientHandler.status()}", // @formatter:on ) } @@ -214,7 +218,7 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothTerminateWrapped( promiseUuid: String, - unusedParameter: String + unusedParameter: String, ) { bleServerHandler.disconnect() bleClientHandler.disconnect() @@ -222,17 +226,16 @@ class WalletJsBridge( resolvePromise(promiseUuid, "true") } - @JavascriptInterface @SuppressLint("unused") fun bluetoothCreateServerWrapped( promiseUuid: String, - serviceUuid: String + serviceUuid: String, ) { bleServerHandler.createServer( serviceUuid = serviceUuid, success = { resolvePromise(promiseUuid, "true") }, - failure = { rejectPromise(promiseUuid, "false") } + failure = { rejectPromise(promiseUuid, "false") }, ) } @@ -245,7 +248,7 @@ class WalletJsBridge( bleClientHandler.createClient( serviceUuid = serviceUuid, success = { resolvePromise(promiseUuid, "true") }, - failure = { rejectPromise(promiseUuid, "false") } + failure = { rejectPromise(promiseUuid, "false") }, ) } @@ -253,14 +256,14 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothSendToServerWrapped( promiseUuid: String, - rawParameter: String + rawParameter: String, ) { val parameter = JSONArray(rawParameter).toByteArray() bleClientHandler.sendToServer( parameter, success = { resolvePromise(promiseUuid, "true") }, - failure = { rejectPromise(promiseUuid, "false") } + failure = { rejectPromise(promiseUuid, "false") }, ) } @@ -268,14 +271,14 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothSendToClientWrapped( promiseUuid: String, - rawParameter: String + rawParameter: String, ) { val parameter = JSONArray(rawParameter).toByteArray() bleServerHandler.sendToClient( parameter, success = { resolvePromise(promiseUuid, "true") }, - failure = { rejectPromise(promiseUuid, "false") } + failure = { rejectPromise(promiseUuid, "false") }, ) } @@ -283,11 +286,11 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothReceiveFromClientWrapped( promiseUuid: String, - unusedParameter: String + unusedParameter: String, ) { bleServerHandler.receiveFromClient( success = { resolvePromise(promiseUuid, JSONArray(it).toString()) }, - failure = { rejectPromise(promiseUuid, "null") } + failure = { rejectPromise(promiseUuid, "null") }, ) } @@ -295,11 +298,11 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothReceiveFromServerWrapped( promiseUuid: String, - unusedParameter: String + unusedParameter: String, ) { bleClientHandler.receiveFromServer( success = { resolvePromise(promiseUuid, JSONArray(it).toString()) }, - failure = { rejectPromise(promiseUuid, "false") } + failure = { rejectPromise(promiseUuid, "false") }, ) } @@ -315,37 +318,37 @@ class WalletJsBridge( @SuppressLint("unused") fun bluetoothGetMode(): String = ServiceCharacteristic.mode.name - private fun resolvePromise( promiseUuid: String, - result: String + result: String, ) { dispatcher.dispatch(EmptyCoroutineContext) { val wrapped = JSONObject.wrap(result) webView.evaluateJavascript( - "${JAVASCRIPT_BRIDGE_NAME}.__resolve__('$promiseUuid', '$wrapped')" + "${JAVASCRIPT_BRIDGE_NAME}.__resolve__('$promiseUuid', '$wrapped')", ) {} } } private fun rejectPromise( promiseUuid: String, - result: String + result: String, ) { dispatcher.dispatch(EmptyCoroutineContext) { val wrapped = JSONObject.wrap(result) webView.evaluateJavascript( - "${JAVASCRIPT_BRIDGE_NAME}.__reject__('$promiseUuid', '$wrapped')" + "${JAVASCRIPT_BRIDGE_NAME}.__reject__('$promiseUuid', '$wrapped')", ) {} } } } -private fun JSONArray.toByteArray(): ByteArray = (0 until length()).mapNotNull { index -> - val value = get(index) - if (value is Int) { - value.toByte() - } else { - null - } -}.toByteArray() +private fun JSONArray.toByteArray(): ByteArray = + (0 until length()).mapNotNull { index -> + val value = get(index) + if (value is Int) { + value.toByte() + } else { + null + } + }.toByteArray() diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainer.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainer.kt index 8e28faa..31c27ad 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainer.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainer.kt @@ -6,12 +6,12 @@ interface NavigatorCredentialsContainer { fun create( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) fun get( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) } diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerAndroid.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerAndroid.kt index 75d28b2..64a092b 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerAndroid.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerAndroid.kt @@ -18,20 +18,20 @@ private const val REGISTRATION_RESPONSE_BUNDLE_KEY = class NavigatorCredentialsContainerAndroid( val activity: Activity, ) : NavigatorCredentialsContainer { - val manager = CredentialManager.create(activity) override fun create( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { CoroutineScope(EmptyCoroutineContext).launch { try { - val result = manager.createCredential( - context = activity, - request = options.toCreateRequestOption() - ) + val result = + manager.createCredential( + context = activity, + request = options.toCreateRequestOption(), + ) val rawResult = result.data.getString(REGISTRATION_RESPONSE_BUNDLE_KEY) ?: "" @@ -45,20 +45,22 @@ class NavigatorCredentialsContainerAndroid( override fun get( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { CoroutineScope(EmptyCoroutineContext).launch { try { - val result = manager.getCredential( - context = activity, - request = options.toGetRequestOption() - ) + val result = + manager.getCredential( + context = activity, + request = options.toGetRequestOption(), + ) - val rawResult = if(result.credential is PublicKeyCredential) { - (result.credential as PublicKeyCredential).authenticationResponseJson - } else { - """ {"error":"no public key credential returned", "actual":"$result"} """.trim() - } + val rawResult = + if (result.credential is PublicKeyCredential) { + (result.credential as PublicKeyCredential).authenticationResponseJson + } else { + """ {"error":"no public key credential returned", "actual":"$result"} """.trim() + } successCallback(JSONObject(rawResult)) } catch (th: Throwable) { @@ -70,13 +72,15 @@ class NavigatorCredentialsContainerAndroid( private fun JSONObject.toCreateRequestOption(): CreateCredentialRequest = CreatePublicKeyCredentialRequest( - getJSONObject("publicKey").toString() + getJSONObject("publicKey").toString(), ) -private fun JSONObject.toGetRequestOption(): GetCredentialRequest = GetCredentialRequest( - credentialOptions = listOf( - GetPublicKeyCredentialOption( - requestJson = getString("publicKey").toString(), - ) +private fun JSONObject.toGetRequestOption(): GetCredentialRequest = + GetCredentialRequest( + credentialOptions = + listOf( + GetPublicKeyCredentialOption( + requestJson = getString("publicKey").toString(), + ), + ), ) -) diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerYubico.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerYubico.kt index 096ae99..cb5eec6 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerYubico.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/NavigatorCredentialsContainerYubico.kt @@ -63,25 +63,27 @@ class NavigatorCredentialsContainerYubico( ) : NavigatorCredentialsContainer { private val manager: YubiKitManager = YubiKitManager(activity) - private val usbListener: Callback = Callback { device -> - deviceConnected(device) - } + private val usbListener: Callback = + Callback { device -> + deviceConnected(device) + } - private val nfcListener: Callback = Callback { device -> - deviceConnected(device) - } + private val nfcListener: Callback = + Callback { device -> + deviceConnected(device) + } private fun startDiscoveries() { manager.startUsbDiscovery( UsbConfiguration().handlePermissions(true), - usbListener + usbListener, ) try { manager.startNfcDiscovery( NfcConfiguration().timeout(10_000), activity, - nfcListener + nfcListener, ) } catch (e: NfcNotAvailable) { Log.i(tagForLog, "No NFC, ignoring.", e) @@ -93,36 +95,41 @@ class NavigatorCredentialsContainerYubico( override fun create( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { Log.i(tagForLog, "yubico create implementation called.") startDiscoveries() - lastOperation = CreateOperation( - options = options, - success = successCallback, - failure = failureCallback - ) + lastOperation = + CreateOperation( + options = options, + success = successCallback, + failure = failureCallback, + ) } override fun get( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { Log.i(tagForLog, "yubico get implementation called.") startDiscoveries() - lastOperation = GetOperation( - options = options, - success = successCallback, - failure = failureCallback - ) + lastOperation = + GetOperation( + options = options, + success = successCallback, + failure = failureCallback, + ) } - private fun askForPin(operation: Operation, device: YubiKeyDevice) { + private fun askForPin( + operation: Operation, + device: YubiKeyDevice, + ) { // ask user for pin requestPin { providedPin -> if (providedPin != null) { @@ -130,8 +137,8 @@ class NavigatorCredentialsContainerYubico( } else { operation.failure( UserNotAuthenticatedException( - "User did enter empty pin." - ) + "User did enter empty pin.", + ), ) } } @@ -140,21 +147,23 @@ class NavigatorCredentialsContainerYubico( private fun routeToCorrectMethodWithPin( operation: Operation, device: YubiKeyDevice, - pin: String? + pin: String?, ) { try { when (operation) { - is CreateOperation -> createWithDevice( - device, - operation, - pin - ) + is CreateOperation -> + createWithDevice( + device, + operation, + pin, + ) - is GetOperation -> getWithDevice( - device, - operation, - pin - ) + is GetOperation -> + getWithDevice( + device, + operation, + pin, + ) } } catch (e: Throwable) { // 👀 - WHy? TODO @@ -171,14 +180,14 @@ class NavigatorCredentialsContainerYubico( private fun createWithDevice( device: YubiKeyDevice, operation: CreateOperation, - pin: String? + pin: String?, ) { Ctap2Session.create(device) { result: Result -> if (result.isSuccess) { createWithCtap2Session( result.value, operation, - pin + pin, ) } else { Log.e(tagForLog, "Couldn't create session.", result.actualError) @@ -190,7 +199,7 @@ class NavigatorCredentialsContainerYubico( private fun createWithCtap2Session( session: Ctap2Session, operation: CreateOperation, - pin: String? + pin: String?, ) { val client = BasicWebAuthnClient(session) @@ -198,30 +207,32 @@ class NavigatorCredentialsContainerYubico( val publicKey = createOptions.publicKey!! val kitOptions = PublicKeyCredentialCreationOptions.fromMap(publicKey.toMap()) val domain = publicKey.rp?.id ?: "" - val json: ByteArray = getClientOptions( - type = "webauthn.create", - origin = domain, - challenge = encodeToString( - kitOptions.challenge, - NO_PADDING or NO_WRAP or URL_SAFE + val json: ByteArray = + getClientOptions( + type = "webauthn.create", + origin = domain, + challenge = + encodeToString( + kitOptions.challenge, + NO_PADDING or NO_WRAP or URL_SAFE, + ), ) - ) val enterprise = null val state = null try { - val result: PublicKeyCredential = client.makeCredential( - json, - kitOptions, - domain, - pin?.toCharArray(), - enterprise, - state - ) + val result: PublicKeyCredential = + client.makeCredential( + json, + kitOptions, + domain, + pin?.toCharArray(), + enterprise, + state, + ) Log.i(tagForLog, "Done, created $result.") operation.success(JSONObject(result.toMap())) - } catch (ctap: CtapException) { Log.e(tagForLog, "Protocol exception: '${ctap.ctapError.toHumanReadable()}'.", ctap) operation.failure(ctap) @@ -238,14 +249,14 @@ class NavigatorCredentialsContainerYubico( private fun getWithDevice( device: YubiKeyDevice, operation: GetOperation, - pin: String? + pin: String?, ) { Ctap2Session.create(device) { result: Result -> if (result.isSuccess) { getWithCtap2Session( result.value, operation, - pin + pin, ) } else { Log.e(tagForLog, "Couldn't get session.", result.actualError) @@ -257,7 +268,7 @@ class NavigatorCredentialsContainerYubico( private fun getWithCtap2Session( session: Ctap2Session, operation: GetOperation, - pin: String? + pin: String?, ) { val client = BasicWebAuthnClient(session) @@ -265,23 +276,26 @@ class NavigatorCredentialsContainerYubico( val publicKey = getOptions.publicKey!! val kitOptions = PublicKeyCredentialRequestOptions.fromMap(publicKey.toMap()) val domain = publicKey.rpId ?: "" - val json = getClientOptions( - type = "webauthn.get", - origin = domain, - challenge = encodeToString( - kitOptions.challenge, - NO_PADDING or NO_WRAP or URL_SAFE + val json = + getClientOptions( + type = "webauthn.get", + origin = domain, + challenge = + encodeToString( + kitOptions.challenge, + NO_PADDING or NO_WRAP or URL_SAFE, + ), ) - ) val enterprise = null try { - val result = client.getAssertion( - json, - kitOptions, - domain, - pin?.toCharArray(), - enterprise, - ) + val result = + client.getAssertion( + json, + kitOptions, + domain, + pin?.toCharArray(), + enterprise, + ) Log.i(tagForLog, "Done, got $result.") operation.success(JSONObject(result.toMap())) @@ -314,19 +328,19 @@ class NavigatorCredentialsContainerYubico( PublicKeyCredentialDescriptor( "public-key", credential.rawId, - null - ) + null, + ), ) get( JSONObject(mapOf("publicKey" to kitOptions.toMap(SerializationType.JSON))), operation.success, - operation.failure + operation.failure, ) }, failure = { operation.failure(Throwable()) - } + }, ) } @@ -337,15 +351,19 @@ class NavigatorCredentialsContainerYubico( ) { Dispatchers.Main.dispatch(EmptyCoroutineContext) { val items = available.users.map { it.displayName }.toTypedArray() - val listener = object : DialogInterface.OnClickListener { - override fun onClick(dialog: DialogInterface?, which: Int) { - val credential = available.select(which) - Log.i(tagForLog, "credential selected: $credential") - dialog?.dismiss() - - success(credential) + val listener = + object : DialogInterface.OnClickListener { + override fun onClick( + dialog: DialogInterface?, + which: Int, + ) { + val credential = available.select(which) + Log.i(tagForLog, "credential selected: $credential") + dialog?.dismiss() + + success(credential) + } } - } AlertDialog.Builder(activity) .setTitle("Select one") @@ -362,113 +380,116 @@ class NavigatorCredentialsContainerYubico( private fun requestPin(callback: (String?) -> Unit) { Dispatchers.Main.dispatch(EmptyCoroutineContext) { - - val pinEdit = EditText(activity).apply { - hint = "PIN" - maxLines = 1 - minLines = 1 - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } + val pinEdit = + EditText(activity).apply { + hint = "PIN" + maxLines = 1 + minLines = 1 + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } // TODO: make into fancy bottom drawer - val dialog = AlertDialog.Builder(activity) - .setTitle("Pin Required") - .setView(pinEdit) - .setPositiveButton(android.R.string.ok) { dialog, which -> - Log.i(tagForLog, "PIN entered.") - dialog.dismiss() - callback(pinEdit.text.toString()) - } - .setNegativeButton(android.R.string.cancel) { dialog, which -> - Log.i(tagForLog, "PIN entry cancelled.") - dialog.dismiss() - callback(null) - }.show() + val dialog = + AlertDialog.Builder(activity) + .setTitle("Pin Required") + .setView(pinEdit) + .setPositiveButton(android.R.string.ok) { dialog, which -> + Log.i(tagForLog, "PIN entered.") + dialog.dismiss() + callback(pinEdit.text.toString()) + } + .setNegativeButton(android.R.string.cancel) { dialog, which -> + Log.i(tagForLog, "PIN entry cancelled.") + dialog.dismiss() + callback(null) + }.show() pinEdit.setOnEditorActionListener( object : TextView.OnEditorActionListener { override fun onEditorAction( v: TextView, actionId: Int, - event: KeyEvent? + event: KeyEvent?, ): Boolean { dialog.dismiss() callback(v.text.toString()) return true } - } + }, ) } } } @Suppress("DEPRECATION") -private fun Byte.toHumanReadable(): String = when (this) { - CtapException.ERR_SUCCESS -> "ERR_SUCCESS" - CtapException.ERR_INVALID_COMMAND -> "ERR_INVALID_COMMAND" - CtapException.ERR_INVALID_PARAMETER -> "ERR_INVALID_PARAMETER" - CtapException.ERR_INVALID_LENGTH -> "ERR_INVALID_LENGTH" - CtapException.ERR_INVALID_SEQ -> "ERR_INVALID_SEQ" - CtapException.ERR_TIMEOUT -> "ERR_TIMEOUT" - CtapException.ERR_CHANNEL_BUSY -> "ERR_CHANNEL_BUSY" - CtapException.ERR_LOCK_REQUIRED -> "ERR_LOCK_REQUIRED" - CtapException.ERR_INVALID_CHANNEL -> "ERR_INVALID_CHANNEL" - CtapException.ERR_CBOR_UNEXPECTED_TYPE -> "ERR_CBOR_UNEXPECTED_TYPE" - CtapException.ERR_INVALID_CBOR -> "ERR_INVALID_CBOR" - CtapException.ERR_MISSING_PARAMETER -> "ERR_MISSING_PARAMETER" - CtapException.ERR_LIMIT_EXCEEDED -> "ERR_LIMIT_EXCEEDED" - CtapException.ERR_UNSUPPORTED_EXTENSION -> "ERR_UNSUPPORTED_EXTENSION" - CtapException.ERR_FP_DATABASE_FULL -> "ERR_FP_DATABASE_FULL" - CtapException.ERR_CREDENTIAL_EXCLUDED -> "ERR_CREDENTIAL_EXCLUDED" - CtapException.ERR_PROCESSING -> "ERR_PROCESSING" - CtapException.ERR_INVALID_CREDENTIAL -> "ERR_INVALID_CREDENTIAL" - CtapException.ERR_USER_ACTION_PENDING -> "ERR_USER_ACTION_PENDING" - CtapException.ERR_OPERATION_PENDING -> "ERR_OPERATION_PENDING" - CtapException.ERR_NO_OPERATIONS -> "ERR_NO_OPERATIONS" - CtapException.ERR_UNSUPPORTED_ALGORITHM -> "ERR_UNSUPPORTED_ALGORITHM" - CtapException.ERR_OPERATION_DENIED -> "ERR_OPERATION_DENIED" - CtapException.ERR_KEY_STORE_FULL -> "ERR_KEY_STORE_FULL" - CtapException.ERR_NOT_BUSY -> "ERR_NOT_BUSY" - CtapException.ERR_NO_OPERATION_PENDING -> "ERR_NO_OPERATION_PENDING" - CtapException.ERR_UNSUPPORTED_OPTION -> "ERR_UNSUPPORTED_OPTION" - CtapException.ERR_INVALID_OPTION -> "ERR_INVALID_OPTION" - CtapException.ERR_KEEPALIVE_CANCEL -> "ERR_KEEPALIVE_CANCEL" - CtapException.ERR_NO_CREDENTIALS -> "ERR_NO_CREDENTIALS" - CtapException.ERR_USER_ACTION_TIMEOUT -> "ERR_USER_ACTION_TIMEOUT" - CtapException.ERR_NOT_ALLOWED -> "ERR_NOT_ALLOWED" - CtapException.ERR_PIN_INVALID -> "ERR_PIN_INVALID" - CtapException.ERR_PIN_BLOCKED -> "ERR_PIN_BLOCKED" - CtapException.ERR_PIN_AUTH_INVALID -> "ERR_PIN_AUTH_INVALID" - CtapException.ERR_PIN_AUTH_BLOCKED -> "ERR_PIN_AUTH_BLOCKED" - CtapException.ERR_PIN_NOT_SET -> "ERR_PIN_NOT_SET" - CtapException.ERR_PIN_REQUIRED -> "ERR_PIN_REQUIRED" - CtapException.ERR_PIN_POLICY_VIOLATION -> "ERR_PIN_POLICY_VIOLATION" - CtapException.ERR_PIN_TOKEN_EXPIRED -> "ERR_PIN_TOKEN_EXPIRED" - CtapException.ERR_REQUEST_TOO_LARGE -> "ERR_REQUEST_TOO_LARGE" - CtapException.ERR_ACTION_TIMEOUT -> "ERR_ACTION_TIMEOUT" - CtapException.ERR_UP_REQUIRED -> "ERR_UP_REQUIRED" - CtapException.ERR_UV_BLOCKED -> "ERR_UV_BLOCKED" - CtapException.ERR_INTEGRITY_FAILURE -> "ERR_INTEGRITY_FAILURE" - CtapException.ERR_INVALID_SUBCOMMAND -> "ERR_INVALID_SUBCOMMAND" - CtapException.ERR_UV_INVALID -> "ERR_UV_INVALID" - CtapException.ERR_UNAUTHORIZED_PERMISSION -> "ERR_UNAUTHORIZED_PERMISSION" - CtapException.ERR_OTHER -> "ERR_OTHER" - CtapException.ERR_SPEC_LAST -> "ERR_SPEC_LAST" - CtapException.ERR_EXTENSION_FIRST -> "ERR_EXTENSION_FIRST" - CtapException.ERR_EXTENSION_LAST -> "ERR_EXTENSION_LAST" - CtapException.ERR_VENDOR_FIRST -> "ERR_VENDOR_FIRST" - CtapException.ERR_VENDOR_LAST -> "ERR_VENDOR_LAST" - else -> "Unknown CTAP ERROR" -} +private fun Byte.toHumanReadable(): String = + when (this) { + CtapException.ERR_SUCCESS -> "ERR_SUCCESS" + CtapException.ERR_INVALID_COMMAND -> "ERR_INVALID_COMMAND" + CtapException.ERR_INVALID_PARAMETER -> "ERR_INVALID_PARAMETER" + CtapException.ERR_INVALID_LENGTH -> "ERR_INVALID_LENGTH" + CtapException.ERR_INVALID_SEQ -> "ERR_INVALID_SEQ" + CtapException.ERR_TIMEOUT -> "ERR_TIMEOUT" + CtapException.ERR_CHANNEL_BUSY -> "ERR_CHANNEL_BUSY" + CtapException.ERR_LOCK_REQUIRED -> "ERR_LOCK_REQUIRED" + CtapException.ERR_INVALID_CHANNEL -> "ERR_INVALID_CHANNEL" + CtapException.ERR_CBOR_UNEXPECTED_TYPE -> "ERR_CBOR_UNEXPECTED_TYPE" + CtapException.ERR_INVALID_CBOR -> "ERR_INVALID_CBOR" + CtapException.ERR_MISSING_PARAMETER -> "ERR_MISSING_PARAMETER" + CtapException.ERR_LIMIT_EXCEEDED -> "ERR_LIMIT_EXCEEDED" + CtapException.ERR_UNSUPPORTED_EXTENSION -> "ERR_UNSUPPORTED_EXTENSION" + CtapException.ERR_FP_DATABASE_FULL -> "ERR_FP_DATABASE_FULL" + CtapException.ERR_CREDENTIAL_EXCLUDED -> "ERR_CREDENTIAL_EXCLUDED" + CtapException.ERR_PROCESSING -> "ERR_PROCESSING" + CtapException.ERR_INVALID_CREDENTIAL -> "ERR_INVALID_CREDENTIAL" + CtapException.ERR_USER_ACTION_PENDING -> "ERR_USER_ACTION_PENDING" + CtapException.ERR_OPERATION_PENDING -> "ERR_OPERATION_PENDING" + CtapException.ERR_NO_OPERATIONS -> "ERR_NO_OPERATIONS" + CtapException.ERR_UNSUPPORTED_ALGORITHM -> "ERR_UNSUPPORTED_ALGORITHM" + CtapException.ERR_OPERATION_DENIED -> "ERR_OPERATION_DENIED" + CtapException.ERR_KEY_STORE_FULL -> "ERR_KEY_STORE_FULL" + CtapException.ERR_NOT_BUSY -> "ERR_NOT_BUSY" + CtapException.ERR_NO_OPERATION_PENDING -> "ERR_NO_OPERATION_PENDING" + CtapException.ERR_UNSUPPORTED_OPTION -> "ERR_UNSUPPORTED_OPTION" + CtapException.ERR_INVALID_OPTION -> "ERR_INVALID_OPTION" + CtapException.ERR_KEEPALIVE_CANCEL -> "ERR_KEEPALIVE_CANCEL" + CtapException.ERR_NO_CREDENTIALS -> "ERR_NO_CREDENTIALS" + CtapException.ERR_USER_ACTION_TIMEOUT -> "ERR_USER_ACTION_TIMEOUT" + CtapException.ERR_NOT_ALLOWED -> "ERR_NOT_ALLOWED" + CtapException.ERR_PIN_INVALID -> "ERR_PIN_INVALID" + CtapException.ERR_PIN_BLOCKED -> "ERR_PIN_BLOCKED" + CtapException.ERR_PIN_AUTH_INVALID -> "ERR_PIN_AUTH_INVALID" + CtapException.ERR_PIN_AUTH_BLOCKED -> "ERR_PIN_AUTH_BLOCKED" + CtapException.ERR_PIN_NOT_SET -> "ERR_PIN_NOT_SET" + CtapException.ERR_PIN_REQUIRED -> "ERR_PIN_REQUIRED" + CtapException.ERR_PIN_POLICY_VIOLATION -> "ERR_PIN_POLICY_VIOLATION" + CtapException.ERR_PIN_TOKEN_EXPIRED -> "ERR_PIN_TOKEN_EXPIRED" + CtapException.ERR_REQUEST_TOO_LARGE -> "ERR_REQUEST_TOO_LARGE" + CtapException.ERR_ACTION_TIMEOUT -> "ERR_ACTION_TIMEOUT" + CtapException.ERR_UP_REQUIRED -> "ERR_UP_REQUIRED" + CtapException.ERR_UV_BLOCKED -> "ERR_UV_BLOCKED" + CtapException.ERR_INTEGRITY_FAILURE -> "ERR_INTEGRITY_FAILURE" + CtapException.ERR_INVALID_SUBCOMMAND -> "ERR_INVALID_SUBCOMMAND" + CtapException.ERR_UV_INVALID -> "ERR_UV_INVALID" + CtapException.ERR_UNAUTHORIZED_PERMISSION -> "ERR_UNAUTHORIZED_PERMISSION" + CtapException.ERR_OTHER -> "ERR_OTHER" + CtapException.ERR_SPEC_LAST -> "ERR_SPEC_LAST" + CtapException.ERR_EXTENSION_FIRST -> "ERR_EXTENSION_FIRST" + CtapException.ERR_EXTENSION_LAST -> "ERR_EXTENSION_LAST" + CtapException.ERR_VENDOR_FIRST -> "ERR_VENDOR_FIRST" + CtapException.ERR_VENDOR_LAST -> "ERR_VENDOR_LAST" + else -> "Unknown CTAP ERROR" + } private val Result.actualError: Throwable get() { - val error: Throwable = try { - value - IllegalStateException("No error found.") - } catch (actualError: Throwable) { - actualError - } + val error: Throwable = + try { + value + IllegalStateException("No error found.") + } catch (actualError: Throwable) { + actualError + } return error } @@ -485,7 +506,9 @@ private val JSONObject.rp: JSONObject? private val JSONObject.id: String? get() = getOrNull("id") -private fun getClientOptions(type: String, challenge: String, origin: String) = - "{\"type\":\"$type\",\"challenge\":\"$challenge\",\"origin\":\"https://$origin\"}" - .toByteArray() - +private fun getClientOptions( + type: String, + challenge: String, + origin: String, +) = "{\"type\":\"$type\",\"challenge\":\"$challenge\",\"origin\":\"https://$origin\"}" + .toByteArray() diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/json/Extensions.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/json/Extensions.kt index 6c1acbf..4b9a952 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/json/Extensions.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/json/Extensions.kt @@ -10,25 +10,24 @@ import org.json.JSONArray import org.json.JSONException import org.json.JSONObject - fun ByteArray.b64(): String = encodeToString(this, NO_WRAP or NO_PADDING or URL_SAFE) -inline fun JSONObject.getOrNull(name: String): T? = try { - val value = get(name) - if (value is T) { - value - } else { - Log.w( - tagForLog, - "Name $name of $this is not of type ${T::class.java.simpleName} but of ${value.javaClass.simpleName}." - ) +inline fun JSONObject.getOrNull(name: String): T? = + try { + val value = get(name) + if (value is T) { + value + } else { + Log.w( + tagForLog, + "Name $name of $this is not of type ${T::class.java.simpleName} but of ${value.javaClass.simpleName}.", + ) + null + } + } catch (e: JSONException) { + Log.e(tagForLog, "Name $name not found in $this.", e) null } -} catch (e: JSONException) { - Log.e(tagForLog, "Name $name not found in $this.", e) - null -} - fun JSONObject.toMap(): Map { val result = mutableMapOf() @@ -40,26 +39,26 @@ fun JSONObject.toMap(): Map { if (value.names() == null || names?.length() == 0) { result[key] = "" } else { - val bytesOrObject: Map = value.toMap() val keysAreIndexes = bytesOrObject.keys.mapNotNull { it.toIntOrNull() }.isNotEmpty() if (keysAreIndexes) { // make string - val string = ByteArray( - bytesOrObject.size - ) { index -> - val maybeByte = bytesOrObject["$index"] - if (maybeByte is Int) { - maybeByte.toByte() - } else { - Log.w( - tagForLog, - "Index $index of name $key is not a byte but $maybeByte instead." - ) - 0 - } - }.b64() + val string = + ByteArray( + bytesOrObject.size, + ) { index -> + val maybeByte = bytesOrObject["$index"] + if (maybeByte is Int) { + maybeByte.toByte() + } else { + Log.w( + tagForLog, + "Index $index of name $key is not a byte but $maybeByte instead.", + ) + 0 + } + }.b64() result[key] = string } else { @@ -80,33 +79,38 @@ fun JSONObject.toMap(): Map { return result } -fun JSONArray.toList(): List = MutableList(length()) { index -> - when (val value = get(index)) { - is JSONObject -> value.toMap() - is JSONArray -> value.toList() - else -> value +fun JSONArray.toList(): List = + MutableList(length()) { index -> + when (val value = get(index)) { + is JSONObject -> value.toMap() + is JSONArray -> value.toList() + else -> value + } } -} -fun JSONObject.getNested(path: String): Any? = if ("." in path) { - val all = path.split(".") - val first = all.first() +fun JSONObject.getNested(path: String): Any? = + if ("." in path) { + val all = path.split(".") + val first = all.first() - if (has(first)) { - val rest = all.drop(1).joinToString(separator = ".") - getJSONObject(first).getNested(rest) + if (has(first)) { + val rest = all.drop(1).joinToString(separator = ".") + getJSONObject(first).getNested(rest) + } else { + null + } } else { - null - } -} else { - try { - get(path) - } catch (ex: JSONException) { - null + try { + get(path) + } catch (ex: JSONException) { + null + } } -} -fun JSONObject.setNested(path: String, value: Any?) { +fun JSONObject.setNested( + path: String, + value: Any?, +) { if ("." in path) { val all = path.split(".") val first = all.first() diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebChromeClient.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebChromeClient.kt index ba8ab2c..4d7adbe 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebChromeClient.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebChromeClient.kt @@ -15,13 +15,13 @@ import io.yubicolabs.wwwwallet.tagForLog private const val WEBKIT_VIDEO_PERMISSION = "android.webkit.resource.VIDEO_CAPTURE" class WalletWebChromeClient( - private val activity: ComponentActivity + private val activity: ComponentActivity, ) : WebChromeClient() { override fun onJsAlert( view: WebView?, url: String?, message: String?, - result: JsResult? + result: JsResult?, ): Boolean { Log.e("WEBVIEW", message ?: "<>") @@ -45,11 +45,12 @@ class WalletWebChromeClient( ) { when (ContextCompat.checkSelfPermission(context, "android.permission.CAMERA")) { PackageManager.PERMISSION_GRANTED -> request.grant(arrayOf(resource)) - PackageManager.PERMISSION_DENIED -> requestPermission( - "android.permission.CAMERA", - { request.grant(arrayOf(resource)) }, - { request.deny() } - ) + PackageManager.PERMISSION_DENIED -> + requestPermission( + "android.permission.CAMERA", + { request.grant(arrayOf(resource)) }, + { request.deny() }, + ) } } @@ -57,7 +58,11 @@ class WalletWebChromeClient( super.onPermissionRequestCanceled(request) } - private fun requestPermission(input: String, granted: () -> Unit, denied: () -> Unit) { + private fun requestPermission( + input: String, + granted: () -> Unit, + denied: () -> Unit, + ) { permissionGranted = granted permissionDenied = denied diff --git a/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebViewClient.kt b/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebViewClient.kt index f495a7c..5ca2be1 100644 --- a/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebViewClient.kt +++ b/wrapper/src/main/java/io/yubicolabs/wwwwallet/webkit/WalletWebViewClient.kt @@ -15,17 +15,18 @@ import io.yubicolabs.wwwwallet.bridging.WalletJsBridge.Companion.JAVASCRIPT_BRID import io.yubicolabs.wwwwallet.tagForLog import java.io.ByteArrayInputStream -class WalletWebViewClient ( - private val activity: Activity -): WebViewClientCompat() { +class WalletWebViewClient( + private val activity: Activity, +) : WebViewClientCompat() { override fun shouldInterceptRequest( view: WebView, - request: WebResourceRequest + request: WebResourceRequest, ): WebResourceResponse? { val response = super.shouldInterceptRequest(view, request) - val requestHeader = request.requestHeaders.map { e -> - "> ${e.key}: ${e.value}" - }.joinToString(separator = "\n") + val requestHeader = + request.requestHeaders.map { e -> + "> ${e.key}: ${e.value}" + }.joinToString(separator = "\n") val debugRequest = "${request.method.uppercase()} ${request.url}\n$requestHeader" Log.i(tagForLog, "intercepting http request: $debugRequest") @@ -34,16 +35,17 @@ class WalletWebViewClient ( "http", "https" -> response else -> { - val url = if (request.url.toString().startsWith("view://")) { - Uri.parse(request.url.toString().replace("view://", "https://")) - } else { - request.url - } + val url = + if (request.url.toString().startsWith("view://")) { + Uri.parse(request.url.toString().replace("view://", "https://")) + } else { + request.url + } try { activity.startActivity(Intent(Intent.ACTION_VIEW, url)) } catch (e: ActivityNotFoundException) { - Log.e(tagForLog, "Could not find activity for ${url}.", e) + Log.e(tagForLog, "Could not find activity for $url.", e) } return if (url.scheme == "openid4vp") { @@ -58,29 +60,32 @@ class WalletWebViewClient ( - """.trim().toByteArray() - ) + """.trim().toByteArray(), + ), ) } } } } - override fun onPageFinished(view: WebView, url: String) { + override fun onPageFinished( + view: WebView, + url: String, + ) { super.onPageFinished(view, url) view.evaluateJavascript("$JAVASCRIPT_BRIDGE_NAME.inject()") { // remove unwanted elements for (unwanted in listOf( - "menu-area", // pid issuer useless menu in wrapped mode - "ReactModalPortal" + "menu-area", + "ReactModalPortal", )) { view.evaluateJavascript( """ - while(document.getElementsByClassName('$unwanted').length > 0) { - document.getElementsByClassName('$unwanted')[0].remove() - } - """.trimIndent() + while(document.getElementsByClassName('$unwanted').length > 0) { + document.getElementsByClassName('$unwanted')[0].remove() + } + """.trimIndent(), ) { Log.i(tagForLog, "Hardening: Deleted $unwanted class from html.") } @@ -93,14 +98,14 @@ class WalletWebViewClient ( )) { view.evaluateJavascript( """ - for (let elem of document.getElementsByTagName("a")) { - if(elem.href.indexOf("$unwanted") > -1 && elem.href.indexOf("view://") == -1) { - let old = elem.href - elem.setAttribute('href', old.replace('https://','view://')) - console.log("Hardening: Redirected from", old, "to", elem.href) - } + for (let elem of document.getElementsByTagName("a")) { + if(elem.href.indexOf("$unwanted") > -1 && elem.href.indexOf("view://") == -1) { + let old = elem.href + elem.setAttribute('href', old.replace('https://','view://')) + console.log("Hardening: Redirected from", old, "to", elem.href) } - """.trimIndent() + } + """.trimIndent(), ) {} } } @@ -109,7 +114,7 @@ class WalletWebViewClient ( override fun onReceivedSslError( view: WebView, handler: SslErrorHandler, - error: SslError + error: SslError, ) { view.evaluateJavascript("console.log('SSL Error: \"$error\"');") {} handler.proceed() diff --git a/wrapper/src/release/java/io/yubicolabs/wwwwallet/bridging/DebugMenuHandler.kt b/wrapper/src/release/java/io/yubicolabs/wwwwallet/bridging/DebugMenuHandler.kt index bdf530e..7126b60 100644 --- a/wrapper/src/release/java/io/yubicolabs/wwwwallet/bridging/DebugMenuHandler.kt +++ b/wrapper/src/release/java/io/yubicolabs/wwwwallet/bridging/DebugMenuHandler.kt @@ -13,7 +13,7 @@ class DebugMenuHandler( jsExecutor( """ console.log("DEBUG MENU IGNORED IN RELEASE BUILD."); - """.trimIndent() + """.trimIndent(), ) {} } } diff --git a/wrapper/src/release/java/io/yubicolabs/wwwwallet/credentials/SoftwareCredentialsContainer.kt b/wrapper/src/release/java/io/yubicolabs/wwwwallet/credentials/SoftwareCredentialsContainer.kt index 9383931..9dc43ef 100644 --- a/wrapper/src/release/java/io/yubicolabs/wwwwallet/credentials/SoftwareCredentialsContainer.kt +++ b/wrapper/src/release/java/io/yubicolabs/wwwwallet/credentials/SoftwareCredentialsContainer.kt @@ -9,14 +9,14 @@ class SoftwareCredentialsContainer : NavigatorCredentialsContainer { override fun create( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { } override fun get( options: JSONObject, successCallback: (JSONObject) -> Unit, - failureCallback: (Throwable) -> Unit + failureCallback: (Throwable) -> Unit, ) { } } diff --git a/wrapper/src/test/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippetTest.kt b/wrapper/src/test/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippetTest.kt index b7880af..ad9b73f 100644 --- a/wrapper/src/test/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippetTest.kt +++ b/wrapper/src/test/java/io/yubicolabs/wwwwallet/bridging/JSCodeSnippetTest.kt @@ -6,25 +6,29 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.Assert.assertNotNull import org.junit.Test -class ParsingTest { +class JSCodeSnippetTest { @Test fun loadRawStringSnippet() { - val code = JSCodeSnippet( - """ + val code = + JSCodeSnippet( + """ window.alert("REPLACEME injected!") - """.trimIndent(), listOf( - "REPLACEME" to "foo bar" + """.trimIndent(), + listOf( + "REPLACEME" to "foo bar", + ), ) - ) assertNotNull(code.code) assertThat( - code.code, not(containsString("REPLACEME")) + code.code, + not(containsString("REPLACEME")), ) assertThat( - code.code, containsString("foo bar") + code.code, + containsString("foo bar"), ) } }