Skip to content

fix(omsdk): run OMID activation on the main thread; disable on failure - #12

Merged
JakubMrozek merged 2 commits into
mainfrom
fix/omid-activation-off-main
Jun 1, 2026
Merged

fix(omsdk): run OMID activation on the main thread; disable on failure#12
JakubMrozek merged 2 commits into
mainfrom
fix/omid-activation-off-main

Conversation

@JakubMrozek

@JakubMrozek JakubMrozek commented Jun 1, 2026

Copy link
Copy Markdown
Member

Problem

A consumer calls KontextAds.createSession() from a background coroutine (Dispatchers.Default, via ChatViewModel.initializeSdk → KontextAdProvider.initKontext). That ran Omid.activate() off the main thread, where OMID's internal new Handler() throws:

RuntimeException: Can't create handler inside thread … that has not called Looper.prepare()

OMID flips its own isActive flag before that throw, so the SDK was left half-initialized — its activate-guard then no-ops every retry. The next session would start a "valid" OMID session whose process-global TreeWalker dereferences a never-initialized WeakReference<Context>, crashing the app on the main thread:

java.lang.NullPointerException: WeakReference.get() on a null object reference
    at com.iab.omid.library.kontextso.internal.k.a
    at com.iab.omid.library.kontextso.walking.TreeWalker.l

Signature in logs: om-session-started {valid=false} (activation threw) → {valid=true} (next session) → crash.

Fix

OmManager:

  • Activation runs on the main thread. When activate() is called off-main it posts Omid.activate() to the main Looper (fire-and-forget, non-blocking — a blocking post-and-await would deadlock a caller that is itself holding the main thread, e.g. runBlocking on main). OMID session start already happens later on the main thread (after preload + ad-done), so the posted activation always completes first.
  • Process-global state + sticky kill-switch. globallyActivated / permanentlyUnavailable @Volatile companion flags. If activation ever fails (throws or leaves OMID inactive), OMID is disabled process-wide so the TreeWalker is never armed and the crash is structurally impossible.

No public API changes. For off-main callers, viewability is restored (not merely suppressed).

Verification

Driven through a multi-Activity character-switch repro that calls createSession() off-main (the field-crash pattern), on an emulator with the real OMID AAR:

  • Before: valid=false → valid=true → crash.
  • After: valid=true on every session across repeated character switches, no crash, no ANR, OMID genuinely active.
  • Also verified the no-AAR path: activation fails → kill-switch → valid=false everywhere, no crash.
  • ./gradlew :build spotlessCheck detekt (incl. unit tests) green.

Consumer note

Calling createSession() on the main thread is a valid immediate workaround on the integration side; this change makes it safe from any thread. Server-side OM kill-switch (omit the om block from the bid) remains the no-release tourniquet.

🤖 Generated with Claude Code

Calling KontextAds.createSession() from a background coroutine ran
Omid.activate() off-main, where OMID's internal new Handler() throws
"Can't create handler ... Looper.prepare()". OMID sets its isActive flag
before that throw, so it was left half-initialized and its guard no-oped
every retry; the next session then started a "valid" OMID session whose
process-global TreeWalker dereferenced a never-initialized
WeakReference<Context> and crashed the app on the main thread.

OmManager now posts Omid.activate() to the main Looper (non-blocking, so
it can't deadlock a caller holding the main thread) and tracks activation
state process-globally with a sticky permanentlyUnavailable guard: if
activation ever fails, OMID is disabled process-wide so the TreeWalker is
never armed. Session start already runs on the main thread after preload,
after the posted activation completes. No public API changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- activate() from a background thread returns without blocking, throwing,
  or deadlocking (guards both the off-main crash and the runBlocking-on-main
  ANR; posted work drained via the main Looper).
- a failed activation is sticky: createSession returns null afterward.
- activation state is shared across OmManager instances (process-global).
- add an internal resetActivationStateForTest() hook + @before reset so the
  static flags don't leak across cases.

The happy path (globallyActivated=true) can't be unit-tested — the OMID AAR
isn't on the test classpath, so Class.forName always throws here; it is
covered by the on-device character-switch repro instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JakubMrozek
JakubMrozek merged commit bdb78d4 into main Jun 1, 2026
1 check passed
@JakubMrozek
JakubMrozek deleted the fix/omid-activation-off-main branch June 1, 2026 14:44
JakubMrozek added a commit to kontextso/sdk-kotlin that referenced this pull request Jun 1, 2026
* chore(deps): bump KontextKit 0.0.7 → 0.0.8 (OMID off-main activation fix)

KontextKit 0.0.8 runs OMID's Omid.activate() on the main thread and
disables OMID process-wide if activation ever fails, fixing a crash when
KontextAds.createSession() is called from a background coroutine. See
kontextso/kontextkit-android#12.

Blocked on the KontextKit 0.0.8 release to Maven Central — draft until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: update SDK version assertions 4.0.3 → 4.0.4

The SDK version flows from libs.versions.toml (sdk-kotlin = 4.0.4) into
BuildConfig.SDK_VERSION → SDKInfo.VERSION; ConfigurationTest and PreloadTest
assert it literally, so they track the version bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant