# 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. **t1** — `onNextAction` fires (`startActivity(NextActivity)`) — **before** the ad Activity finishes.
3. **t2** — `NextActivity.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.

---

## 3. Recommended Pattern — `repeatOnLifecycle(RESUMED)` + small delay + AtomicBoolean one-shot

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.

```kotlin
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):

```kotlin
@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.
