UYLLII

Blog › Engineering

Engineering

Closing an ad tapped the game underneath: a tap guard for AdMob in Capacitor games

By David3 min read
Backpack Battle feature graphic

While playing Backpack Battle on my phone, I closed a rewarded video, and the game immediately did something I hadn't asked for. A button under where the ad's close X had been got pressed.

I checked every other game. None of them had any protection against this, so all of them had the bug.

What's happening

A full-screen AdMob ad is a separate native Activity on top of your app. The player taps the X to close it. The finger goes down on the ad, and the ad starts closing on that touch. By the time the finger lifts, your WebView is visible again, so the lift and the resulting click land on whatever your game has drawn at that spot.

Mobile browsers also synthesise a click after a touch sequence, and sometimes a delayed mouse* sequence too, so even a clean tap can produce a "ghost" event a few hundred milliseconds later.

In Backpack Battle that might buy an item. In an idle game it might trigger a reset. In a word game it might start a new puzzle. Every time, it feels to the player like the game has a mind of its own.

The fix: a tap guard

The fix is a small module, the same file in every game. It works in the capture phase, so it sees events before any game code does:

  1. When an ad (or a consent form, or a privacy options form) is about to open, the game calls adOpening().
  2. From then on, the guard swallows every pointer, touch, mouse and click event, and stops it before the game sees it.
  3. When the ad closes, the game calls adClosed().
  4. The guard keeps swallowing until it sees a new pointerdown that starts at least 350 ms after the ad closed. A fresh, deliberate touch, not the tail of the one that closed the ad.

"Closed" means whichever comes first: the SDK's Dismissed event, or the page becoming visible again. That matters, because I'd learned in an earlier game that Dismissed travels over the Capacitor bridge and can arrive seconds late. Waiting for it alone would freeze the game for no reason.

There's also a 150-second cap. If an ad call hangs and never reports closing, the guard gives up and lets input through, so a broken ad can't lock the game.

Testing it

The guard was tested two ways:

Shipping it everywhere at once

Because every game shares the same platform layer (and most are forks of the Word Search engine), the fix went into all twelve live games the same day, each as a full release. The release check now confirms three things in every bundle: the tap guard is present, there are no test ad IDs, and there's no Play Games init provider (that one's a story of its own).

The rule for future code: every ad or form show goes through adOpening() and adClosed(). New forks inherit it from the engine, so the next game gets it for free.

Why it's worth writing about

This is the kind of bug analytics never shows you. There's no crash and no error, just a player who closed an ad, saw something unexpected happen, and maybe decided the game was janky. If you show full-screen ads in a web-based game, check it now: close an ad with a slow tap and see what your game does.