fix(android): keep the app out from under the system bars

Reported from the first device run: the playback controls are off
screen. `targetSdk 35` is Android 15, which lays every app out
edge-to-edge and ignores the deprecated `statusBarColor` and
`navigationBarColor` the scaffold's theme still sets -- so a
`match_parent` WebView draws the page's bottom band, which on a phone
is the transport *and* the tab bar, underneath the gesture bar.

`applyWindowInsets()` pads the container by
`systemBars | displayCutout | ime` and returns the insets rather than
consuming them, so the WebView is laid out inside them. The keyboard is
in the mask because a search box the keyboard covers is the same bug
one surface over.

The window background goes black to match the app's own default ramp:
that padding is what shows through, and a band of the scaffold's
blue-grey above and below reads as the app failing to fill the screen.

No tier we have can see this class of fault -- a browser viewport has
no system bars, so `phone-shell.spec.ts` at 390x844 renders a shell
that fits at the moment the device is clipping it. Verified only as far
as the APK building; the insets need the next build on a phone.
This commit is contained in:
2026-08-17 02:02:10 -04:00
parent 28eecf0a97
commit d661836347
2 changed files with 70 additions and 1 deletions
@@ -31,9 +31,16 @@ import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.webkit.WebViewAssetLoader;
import org.json.JSONObject;
@@ -88,6 +95,10 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Before anything renders: the page is laid out inside the
// window, and on Android 15 the window is the whole screen.
applyWindowInsets();
// Initialize the native Go library
bridge = new WailsBridge(this);
bridge.initialize();
@@ -892,8 +903,61 @@ public class MainActivity extends AppCompatActivity {
}
}
/**
* Keep the web content inside the safe area.
*
* <p>targetSdk 35 is Android 15, which lays every app out
* edge-to-edge and ignores the {@code statusBarColor} and
* {@code navigationBarColor} this app's theme still sets. The
* WebView is {@code match_parent}, so the page's bottom band -- the
* transport and, on a phone, the tab bar -- was drawn underneath the
* gesture bar and reported from a device as "I can't see the
* playback controls, they seem to be off screen".
*
* <p>No web-tier test can see this: a browser viewport has no system
* bars, so the phone specs at 390x844 render a shell that fits
* while the device does not.
*
* <p>The insets are applied as padding and the window insets are
* returned rather than consumed, so the WebView is laid out inside
* them. {@code ime()} is in the mask because the same reasoning
* covers the keyboard: a focused search box that the keyboard
* covers is the same bug one surface over.
*/
private void applyWindowInsets() {
final View container = findViewById(R.id.main_container);
if (container == null) {
return;
}
ViewCompat.setOnApplyWindowInsetsListener(container, (view, windowInsets) -> {
Insets insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars()
| WindowInsetsCompat.Type.displayCutout()
| WindowInsetsCompat.Type.ime());
view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
return windowInsets;
});
// The padded band shows the window background, which is dark
// (this app's own default ramp is black), so the system's icons
// have to be the light set or they vanish into it. The theme is
// DayNight and would otherwise ask for dark icons in light mode.
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
controller.setAppearanceLightStatusBars(false);
controller.setAppearanceLightNavigationBars(false);
}
@Override
public void onBackPressed() {
// The frontend records every navigation as a history entry, so
// this is the app's own back stack: `canGoBack()` is false only
// at the launch entry, which is where back should leave.
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
@@ -2,7 +2,12 @@
<resources>
<color name="wails_blue">#3574D4</color>
<color name="wails_blue_dark">#2C5FB8</color>
<color name="wails_background">#1B2636</color>
<!-- The window background, which is what the launch screen shows and
what the system-bar padding leaves visible. Black rather than the
scaffold's blue-grey because this app's own default ramp is
black: a band of #1B2636 above and below it reads as the app
failing to fill the screen. -->
<color name="wails_background">#000000</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
</resources>