From 62a497c330d01eb28206d4af4708a0577d08d15b Mon Sep 17 00:00:00 2001 From: JakubMrozek Date: Thu, 21 May 2026 22:40:22 +0200 Subject: [PATCH 1/3] OmSession: use Owner.JAVASCRIPT impression owner for HTML display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v4 KontextKit OMID config used `Owner.NATIVE` as impressionOwner for HTML_DISPLAY sessions on the assumption that the native SDK firing loaded() + impressionOccurred() would stop the JS verification scripts from polling geometry. In practice the JS still polled, and after the hosting WebView was detached from its window the next poll emitted a spurious `geometryChange { reasons: ["notFound"] }`. Both reference implementations use Owner.JAVASCRIPT for HTML display: - IAB OMID Android v1.6.4 demo (`OM-DemoApp/.../AdSessionUtil.java:55`): Owner.JAVASCRIPT, BEGIN_TO_RENDER, mediaOwner = NONE for HTML_DISPLAY. - kontextkit-ios (`Sources/OMSDK/OMManager.swift:107-115`): .javaScriptOwner for both display and video. - v3 sdk-kotlin (`internal/utils/om/WebViewOmSession.kt:75`): same. All four converge: HTML display sessions are JS-impression-owner. The in-iframe `use-omid-display-session` hook (in ads/packages/omsdk) fires `adEvents.loaded(null)` + `adEvents.impressionOccurred()` from the `sessionStart` observer; the native side just constructs the session and calls `start()`. Switching impressionOwner also drops the native AdEvents creation — no longer needed for any creative type, since the verification scripts own loaded/impression for both display and video now. `loaded()` and `impressionOccurred()` on `OmSession` become no-ops kept only for SDK API compatibility (sdk-kotlin still references them today; can be removed in a follow-up). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../kotlin/so/kontext/kit/omsdk/OmSession.kt | 63 +++++++------------ 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt b/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt index 1a8efcb..c7e62ea 100644 --- a/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt +++ b/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt @@ -68,30 +68,32 @@ public class OmSession( val context = createContext.invoke(null, partner, webView, url, "") // Pick creative-type / impression-type / impression-owner / media-events-owner. - // - Display: NATIVE impression owner. SDK fires loaded() + impressionOccurred() - // via AdEvents so the JS verification script does NOT poll geometry and - // does NOT emit a `notFound` geometryChange when the WebView detaches. - // - Video: DEFINED_BY_JAVASCRIPT triple per the IAB OMID Android - // #webview-video docs — JS owns impression + media events. - val nativeOwner = ownerClass.getField("NATIVE").get(null)!! + // Matches v3 sdk-kotlin (`internal/utils/om/WebViewOmSession.kt`) and + // kontextkit-ios (`Sources/OMSDK/OMManager.swift`) — both use + // `Owner.JAVASCRIPT` for the impression owner regardless of creative. + // The JS verification script (use-omid-display-session / + // use-omid-video-session in ads/packages/omsdk) owns firing + // `loaded()` + `impressionOccurred()` from inside the iframe; the + // native side just creates the session. + // - Display: HTML_DISPLAY + BEGIN_TO_RENDER + JS impression + NONE media. + // - Video: DEFINED_BY_JAVASCRIPT triple per the IAB OMID Android + // #webview-video docs. val jsOwner = ownerClass.getField("JAVASCRIPT").get(null)!! val noneOwner = ownerClass.getField("NONE").get(null)!! val omCreativeType: Any val omImpressionType: Any - val impressionOwner: Any val mediaOwner: Any if (creativeType == OmCreativeType.VIDEO) { omCreativeType = creativeTypeClass.getField("DEFINED_BY_JAVASCRIPT").get(null)!! omImpressionType = impressionTypeClass.getField("DEFINED_BY_JAVASCRIPT").get(null)!! - impressionOwner = jsOwner mediaOwner = jsOwner } else { omCreativeType = creativeTypeClass.getField("HTML_DISPLAY").get(null)!! omImpressionType = impressionTypeClass.getField("BEGIN_TO_RENDER").get(null)!! - impressionOwner = nativeOwner mediaOwner = noneOwner } + val impressionOwner: Any = jsOwner // AdSessionConfiguration.createAdSessionConfiguration( // creative, impression, impressionOwner, mediaOwner, isolateVerificationScripts=false @@ -123,15 +125,9 @@ public class OmSession( session = sess - // For NATIVE impression-owner sessions (display), create AdEvents so the - // SDK can fire loaded() + impressionOccurred() from Kotlin. For JS-owner - // sessions (video) AdEvents is still created but loaded/impression are - // owned by the in-iframe verification script and we don't call them. - if (creativeType != OmCreativeType.VIDEO) { - val adEventsClass = Class.forName("com.iab.omid.library.kontextso.adsession.AdEvents") - val createAdEvents = adEventsClass.getMethod("createAdEvents", sessionClass) - adEvents = createAdEvents.invoke(null, sess) - } + // No AdEvents created — JS verification scripts own `loaded()` + + // `impressionOccurred()` for both display and video now that + // `impressionOwner = Owner.JAVASCRIPT` is used unconditionally. } catch (e: ReflectiveOperationException) { android.util.Log.w(TAG, "OM: session init failed", e) session = null @@ -154,36 +150,21 @@ public class OmSession( } /** - * Fires the OMID `loaded` event from native code. Only valid for sessions - * with `Owner.NATIVE` as impressionOwner (display). For JS-owner sessions - * (video) the verification script emits this — calling here is a no-op. - * Must be called after [start]. + * Kept for SDK API compatibility — no-op. All sessions use + * `Owner.JAVASCRIPT` for the impression owner, so `loaded` is fired by + * the JS verification script inside the iframe (see + * `use-omid-display-session` / `use-omid-video-session` in + * ads/packages/omsdk). The native side does not own this event. */ public fun loaded() { - if (loadedFired) return - val ev = adEvents ?: return - try { - ev.javaClass.getMethod("loaded").invoke(ev) - loadedFired = true - } catch (e: ReflectiveOperationException) { - android.util.Log.w(TAG, "OM: AdEvents.loaded failed", e) - } + // intentionally empty — JS owns impressionOwner } /** - * Fires the OMID `impressionOccurred` event from native code. Only valid - * for sessions with `Owner.NATIVE` as impressionOwner (display). Must be - * called after [loaded] and after the ad is actually rendered. + * Kept for SDK API compatibility — no-op. See [loaded]. */ public fun impressionOccurred() { - if (impressionFired) return - val ev = adEvents ?: return - try { - ev.javaClass.getMethod("impressionOccurred").invoke(ev) - impressionFired = true - } catch (e: ReflectiveOperationException) { - android.util.Log.w(TAG, "OM: AdEvents.impressionOccurred failed", e) - } + // intentionally empty — JS owns impressionOwner } public fun retire() { From 3ee903529233a913d4213ecd40baa8eb7f6b6aba Mon Sep 17 00:00:00 2001 From: JakubMrozek Date: Fri, 22 May 2026 02:50:42 +0200 Subject: [PATCH 2/3] fix: drop unused loadedFired/impressionFired flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `OmSession.loaded()` and `impressionOccurred()` became no-ops in the parent commit (`62a497c`) — the JS verification scripts own these events now that `impressionOwner = Owner.JAVASCRIPT` for both display and video. The two private booleans that guarded re-entry against those methods are no longer referenced anywhere; detekt's `UnusedPrivateProperty` flagged them and failed CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt b/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt index c7e62ea..5961869 100644 --- a/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt +++ b/src/main/kotlin/so/kontext/kit/omsdk/OmSession.kt @@ -39,8 +39,6 @@ public class OmSession( private var session: Any? = null private var adEvents: Any? = null private var started = false - private var loadedFired = false - private var impressionFired = false public val isValid: Boolean get() = session != null From f18145fc922eb10f215b5e9aa0c1b6ee5fb18018 Mon Sep 17 00:00:00 2001 From: JakubMrozek Date: Fri, 22 May 2026 03:02:03 +0200 Subject: [PATCH 3/3] Bump to Kotlin 2.1.0, AGP 8.7.3, Gradle 8.9; release 0.0.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns KontextKit's toolchain with sdk-kotlin's 2.1 baseline (used in the v3 2.0.1 release) so consuming apps already on Kotlin 2.x stop hitting compiler-output / metadata mismatches when pulling KontextKit transitively. v4 dropped Ktor (uses HttpURLConnection) so the bump here is purely toolchain — no library API changes. Versions: - Kotlin 1.9.22 → 2.1.0 - AGP 8.6.1 → 8.7.3 (requires Gradle 8.9+) - Gradle 8.7 → 8.9 - detekt 1.23.4 → 1.23.8 (Kotlin 2.x compat) - spotless 6.25.0 → 7.2.1 - kit 0.0.4 → 0.0.5 Verified locally with `./gradlew spotlessCheck detekt test` — clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 7 +++++++ gradle/libs.versions.toml | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0643d46..c205cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.0.5 + +OMID compliance + Kotlin 2.1 baseline. + +* `OmSession`: switch HTML display impression owner from `Owner.NATIVE` to `Owner.JAVASCRIPT` (was wrongly assumed to suppress JS geometry polling — IAB validator flagged the resulting impression events). Now matches v3 sdk-kotlin, kontextkit-ios, and the IAB OMID Android v1.6.4 reference demo: `Owner.JAVASCRIPT` for impression on both display and video; `mediaEventsOwner` stays NONE for display, JAVASCRIPT for video. Native-side `AdEvents` construction dropped — JS verification scripts now own `loaded()` + `impressionOccurred()` for both creative types. `OmSession.loaded()` / `impressionOccurred()` kept as no-ops for SDK API compatibility (consuming SDKs still reference them). +* Toolchain: Kotlin 1.9.22 → **2.1.0**, AGP 8.6.1 → 8.7.3, Gradle 8.7 → 8.9, detekt 1.23.4 → 1.23.8, spotless 6.25.0 → 7.2.1. Aligns with sdk-kotlin 2.1 baseline so consuming apps already on Kotlin 2.x stop hitting compiler-output / metadata mismatches. + ## 0.0.4 * `InstallIdProvider`: new `deviceinfo/InstallIdProvider.kt`. Returns a UUID v7 per-app-install identifier persisted in a dedicated `kontextso` `SharedPreferences` file under the `installId` key. Generated on first call, survives launches, resets only on uninstall / app-data clear. Mirrors iOS `InstallIdProvider` (UserDefaults-backed) so consumer SDKs can thread the same `installId` field through `/init`, `/preload`, `/error`, and `/debug` request payloads. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02f94b3..7c7eaf5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,16 +14,16 @@ # `./gradlew publish -PkitVersion=1.1.0` so CI can cut releases # without bumping this file. Consumed by build.gradle.kts as the # fallback when the `kitVersion` Gradle property is absent. -kit = "0.0.4" +kit = "0.0.5" # ---- Build tooling ---- -agp = "8.6.1" # Android Gradle Plugin -kotlin = "1.9.22" # Kotlin compiler version +agp = "8.7.3" # Android Gradle Plugin +kotlin = "2.1.0" # Kotlin compiler version # ---- Code-quality plugins ---- maven-publish = "0.34.0" # vanniktech maven-publish -spotless = "6.25.0" -detekt = "1.23.4" +spotless = "7.2.1" +detekt = "1.23.8" ktlint = "0.50.0" # consumed inside spotless { ktlint(...) } # ---- AndroidX ---- diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7b71216..fff113c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 11 16:57:10 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists