Fix Android paywall ignoring bottom safe area in fullscreen Unity host#926
Fix Android paywall ignoring bottom safe area in fullscreen Unity host#926tonidero wants to merge 4 commits into
Conversation
…ullscreen The Unity paywall presenter captured system-bar insets from the host activity before showing the dialog. When the host (e.g. Unity with unity.launch-fullscreen=True) hides system bars, getInsets(navigationBars()) returns zero. The dialog opens its own window which still shows the nav bar, so the Compose paywall received a zero bottom inset and drew behind the visible navigation bar. Read insets from the dialog window itself via a stateful OnApplyWindowInsetsListener that caches the first non-zero status/nav bar values and re-injects them after FLAG_LAYOUT_NO_LIMITS is applied. The flag is now applied by the listener once real insets have been captured, avoiding the race where the previous flow set the flag immediately after dialog.show() and made the system report zero insets thereafter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ajpallares
left a comment
There was a problem hiding this comment.
I think this looks good, but I will leave approval to someone with more knowledge in the Android layout system. I have one question though
| * Wraps the PaywallView in a container whose insets listener captures the dialog | ||
| * window's real system-bar insets on first dispatch, then triggers FLAG_LAYOUT_NO_LIMITS. | ||
| * Once the flag is applied, subsequent dispatches arrive with zero insets — the listener | ||
| * re-injects the cached values so the PaywallView's Compose content stays padded. |
There was a problem hiding this comment.
Is it possible for this to change at runtime (e.g. if the device rotates) and have the cached values become invalid?
There was a problem hiding this comment.
Good question! and I checked and this actually reapplies the insets, I believe because on rotation, the entire activity and dialog is recreated. So this shouldn't be a problem.
There was a problem hiding this comment.
Is that always the case in Unity? What if the Activity has android:configChanges=["orientation"]? (Although I believe that is ignored when targeting Android 16.)
| int resourceId = activity.getResources().getIdentifier(resourceName, "dimen", "android"); | ||
| if (resourceId > 0) { | ||
| return activity.getResources().getDimensionPixelSize(resourceId); | ||
| if (Build.VERSION.SDK_INT < 35) { // Build.VERSION_CODES.VANILLA_ICE_CREAM |
There was a problem hiding this comment.
I'd pick either the named constant (VANILLA_ICE_CREAM) or the integer, not both 😛 For instance:
| if (Build.VERSION.SDK_INT < 35) { // Build.VERSION_CODES.VANILLA_ICE_CREAM | |
| if (Build.VERSION.SDK_INT < 35) { |
| * Wraps the PaywallView in a container whose insets listener captures the dialog | ||
| * window's real system-bar insets on first dispatch, then triggers FLAG_LAYOUT_NO_LIMITS. | ||
| * Once the flag is applied, subsequent dispatches arrive with zero insets — the listener | ||
| * re-injects the cached values so the PaywallView's Compose content stays padded. |
There was a problem hiding this comment.
Is that always the case in Unity? What if the Activity has android:configChanges=["orientation"]? (Although I believe that is ignored when targeting Android 16.)
Summary
Bug
The Unity Android paywall captured system-bar insets from the host activity's decor view. When the host hides system bars (e.g. Unity with
unity.launch-fullscreen=True), the activity'sWindowInsetsCompat.getInsets(navigationBars())returns zero. The dialog opens its own window which still shows the nav bar, so the Compose paywall received a zero bottom inset and drew behind the visible nav bar.Fix
Read insets from the dialog window itself via an
OnApplyWindowInsetsListeneron the dialog's content container. The dialog window reports the bars that are actually visible to the dialog, regardless of what the host activity is doing.Flag sequencing
FLAG_LAYOUT_NO_LIMITSis now applied by the listener after the first inset dispatch (previously it was set immediately afterdialog.show()). This avoids mutating window flags during the very first inset traversal.Cache (defensive)
The listener caches the last non-zero typed insets and re-injects them if a dispatch ever arrives with zero values. This is defensive: on API 30+, typed insets (
Type.statusBars()andType.navigationBars()) are descriptive and keep being dispatched with real values across rotation, fold/unfold, and multi-window resize regardless ofFLAG_LAYOUT_NO_LIMITS. The flag only zeroes the legacygetSystemWindowInsets()surface, which this code does not read. The cache covers older APIs and edge dispatches that might transiently carry zero typed values.Screenshots
Note how in Android 34 the dialog doesn't show full screen anymore. This is because we don't apply the
FLAG_LAYOUT_NO_LIMITSflag unless we discover any insets. I think it makes more sense since the default is to not go full screen in Android < 35, as opposed to before, where we were going full screen always. Lmk if you think otherwise.