md2link

In-App Ad × Popup Collision — Partner Guide

DraftMay 22, 2026

In-App Ad × Popup Collision — Partner Guide

Audience: Partners integrating InAppAdManager (interstitial / native-full touchpoints). Problem class: A modal popup (permission request, system dialog, alert, bottom sheet) is triggered from a destination screen's onCreate while an interstitial ad from the previous screen is still showing or in the middle of dismissing. Result: popup renders on top of the running ad, the ad is visually overlapped/canceled, impression is lost, and UX is broken.


1. Why It Happens

InAppAdManager.show(activity, tp) { onNextAction } runs the following sequence:

  1. t0 — Caller activity launches interstitial Activity (AdMob FullScreenContentCallback opens).
  2. t1onNextAction fires (startActivity(NextActivity)) — before the ad Activity finishes.
  3. t2NextActivity.onCreate() runs while the ad is still on top.
  4. t3 — If NextActivity.onCreate (or first composition) immediately calls something modal — requestPermissions(...), AlertDialog.show(), BottomSheet.show() — that popup is queued on the new Activity's window and immediately overlays the ad.
  5. t4 — User dismisses popup → ad is gone, impression lost, sometimes the AdMob session is left in an inconsistent state.

This is not a bug in the SDK — it is the standard Android Activity lifecycle. The fix lives in partner integration code.


2. The Rule

Never trigger blocking UI from onCreate / first composition of a screen that is reached via InAppAdManager.show(..., onNextAction = { startActivity(...) }).

Defer the popup until the ad has fully dismissed and the destination is foregrounded.


Works for permission requests, alert dialogs, bottom sheets — anything modal. The delay(200) absorbs the final frames of ad-Activity teardown so the popup never races the dismiss animation.

class PhoneActivity : BaseMviActivity() {

    // AtomicBoolean — survives config change if scoped to the ViewModel,
    // or use `private val` on the Activity if you want it re-armed on recreate.
    private val oneActionInResume = AtomicBoolean(true)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { PhoneScreen(...) }

        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
                delay(200) // let the interstitial Activity finish tearing down
                if (oneActionInResume.getAndSet(false)) {
                    if (!hasStoragePermission()) {
                        shouldGoSettingsForFallback = false
                        handlePermissionRequest()
                    }
                }
            }
        }
    }
}

Why this works:

  1. repeatOnLifecycle(RESUMED) only runs the block when the Activity is actually RESUMED — i.e. the ad Activity has finished and this Activity is on top of the task. It auto-cancels on STARTED → CREATED transitions, so no leaks.
  2. delay(200) covers the residual frames where the ad Activity is still finishing its dismiss animation / onDestroy. Without this, on slow devices the popup can still land while the ad's window is mid-removal.
  3. AtomicBoolean.getAndSet(false) is a thread-safe one-shot — survives recompositions, rotation, and re-entry into RESUMED (e.g. after user denies permission and returns from the system dialog, the block does not re-fire).

For Compose-only screens (no hosting Activity hook):

@Composable
fun PhoneScreen(...) {
    val oneActionInResume = rememberSaveable { AtomicBoolean(true) }
    val lifecycle = LocalLifecycleOwner.current.lifecycle

    LaunchedEffect(lifecycle) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            delay(200)
            if (oneActionInResume.getAndSet(false)) {
                if (!hasStoragePermission()) {
                    handlePermissionRequest()
                }
            }
        }
    }
}

⚠️ delay(200) is a safety buffer, not a workaround for missing lifecycle handling. Do not tune it up to hide other bugs — 150–250 ms is the working range; anything higher is masking a different problem.


See also: docs/inapp-ad-integration.md for the full InAppAdManager API and touchpoint configuration.