Skip to content

Support edge-to-edge and target Android 16 - #231

Open
dkodr wants to merge 11 commits into
cylonid:devfrom
dkodr:feat/edge-to-edge-target-36
Open

dkodr wants to merge 11 commits into
cylonid:devfrom
dkodr:feat/edge-to-edge-target-36

Conversation

@dkodr

@dkodr dkodr commented Sep 11, 2026

Copy link
Copy Markdown

Android 16 deprecates and disables windowOptOutEdgeToEdgeEnforcement
(behaviour changes), so the opt-out added in e5f97b3 for the Android 15
enforcement stops working the moment the target is raised. Since 31 August 2026
Google Play requires new submissions and updates to target API 36, so raising it
is no longer optional. This handles the insets instead of opting out.

Closes #171. Also fixes #193 and #194. It supersedes #216 and covers the same
ground as #217, which I only found after starting — thanks to @tedgravlin for
both; the back-navigation part here migrates to OnBackPressedDispatcher rather
than only enabling the callback, because at target 36 onBackPressed() is no
longer called at all.

Target and toolchain

  • compileSdk and targetSdk to 36; AGP to 8.13.2 and Gradle to 8.13, the
    minimum that supports API 36. Deliberately still on the 8.x line, since the
    manifest-rewriting build logic does not survive AGP 9.
  • versionCode and versionName are untouched — that is your release step.

Edge-to-edge

  • Removed the opt-out and the hardcoded status bar colours.
  • Content views are padded by systemBars | displayCutout | ime and report those
    types back as Insets.NONE, per the WebView inset guidance, so the
    WebView does not pad them a second time.
  • App bars draw behind the status bar through fitsSystemWindows.
  • setDecorFitsSystemWindows(false) plus transparent bar colours for API 28–34,
    where removing the opt-out alone does nothing.
  • Immersive mode pads by the IME only, so web content reaches the cutout again.
    That is Immersive mode - black status bar #193.
  • WebView back navigation moved to OnBackPressedDispatcher (Input hidden behind keyboard #194 is the
    keyboard part; this is the predictive-back part).

Bars that match the page

Padding the WebView leaves two strips showing the window background, which does
not match the page behind them. After each load the page reports its
theme-color and its computed body background; the first fills the top strip,
the second the bottom, and each bar takes its icon colour from its own side.

One sampled colour is not enough: on a page with a dark header over light
content the mismatched end was off by 224 of 255 per channel. Measured on three
pages, comparing theme-color for both strips, body background for both, and one
each:

page theme-color ×2 body ×2 one each
uniform white 0 / 0 0 / 0 0 / 0
Wikipedia 0 / 19 21 / 10 0 / 10
dark header, light body 0 / 224 224 / 0 0 / 0

Pages that do not handle safe-area insets themselves work the same as any
other, which is why this does not rely on env(safe-area-inset-*).

Visual cleanup

These are separate commits and can be dropped if you would rather keep this PR
to the release blocker.

  • Both colour schemes now come from one seed. The light scheme matched a
    Material tonal-spot scheme seeded with #B71C1C in 92 of 141 values; the dark
    one matched in 21, and its primary was #F0BE6D, an amber from an unrelated
    seed — hence amber switches under a red app bar in dark mode. Both are
    regenerated with material-color-utilities:
    new SchemeTonalSpot(Hct.fromInt(argbFromHex('#B71C1C')), isDark, contrast).
    This does change every dark-mode colour, so it is worth a look.
  • Overflow icon contrast 1.42:1 → 6.57:1; it inherited a dark
    colorControlNormal on the red app bar, below the 3:1 WCAG needs for
    interface icons.
  • Swipe action colours were hardcoded outside the theme: #EF6C00 next to
    the signature red, and #FD2C2C for delete, a third red. Now
    colorErrorContainer and colorTertiaryContainer, which follow the theme and
    have dark variants.
  • Settings margins were 25dp on the left and 45dp on the right, and the
    outsideOverlay scrollbar floated 25dp from the screen edge.
  • List titles were upper-cased and centred inside a weighted column that
    starts after the drag handle, which left them visibly off-centre.
  • Settings actions sat flush against the last setting; two neutral text
    buttons are also no longer a recommended pairing, so Save is filled now.
  • Removed colorSignatureDark and swipe_action_positive, both unreferenced.

Testing

Pixel on Android 17 (API 37, WebView 151) and an API 34 emulator, light and dark
system themes: main screen, settings, a light and a dark page, immersive on and
off, back through WebView history → base URL → background, and a page with a
bottom-anchored input and a sticky bottom bar with the keyboard open.

Not tested: landscape, cutouts on the short edges, and sw >= 600dp, where
target 36 additionally stops honouring the portrait lock.

RoboTests.initializationError and UtilUnitTests.faviconFromWebManifest fail
identically on unmodified dev — the first has no runnable methods, the second
asserts against a live favicon URL that has changed.

Two notes on the build

libadblock-client.so from Edsuns/AdblockAndroid:ad-filter:v0.9.1 has 4 KB
LOAD alignment in all four ABIs. Android 17 shows a compatibility warning
naming it, and since 1 November 2025 Play rejects apps targeting Android 15+
whose native code is not 16 KB aligned. The library has had no release since
v0.9.1, so this needs a rebuilt or replaced dependency — independent of this PR,
but it will block the same release.

Also, preBuild.dependsOn renameManifest moves AndroidManifest.xml out of the
way and only assemble's doLast moves it back, so two assemble tasks in one
Gradle invocation delete it, and a failed build leaves the tree dirty. Worth a
line in the README until it changes.

Android 16 disables windowOptOutEdgeToEdgeEnforcement, so the opt-out added
for the Android 15 enforcement stops working as soon as the target is raised.
Google Play requires new submissions to target API 36 since August 31, 2026.

- Raise compileSdk/targetSdk to 36 and AGP/Gradle to versions supporting it
- Drop the edge-to-edge opt-out and the hardcoded status bar colors
- Pad the content view by system bar, cutout and IME insets, reporting those
  types as zero so WebView does not pad them a second time
- Let the app bars draw behind the status bar via fitsSystemWindows
- In immersive mode pad by the IME only, so web content reaches the cutout
- Move the WebView back handling to OnBackPressedDispatcher, as predictive
  back stops calling onBackPressed at target 36
The inset padding leaves the bar areas showing the window background, which
does not match the page behind them. Read the page colour after load and use
it for those areas and for the bar icon appearance, so transparent bars look
like part of the page on any site, including ones that do not handle
safe-area insets themselves.

Also enable edge-to-edge below API 35, where removing the opt-out has no
effect, and let AppTheme.WebView and AppTheme.News inherit the night theme.
The overflow icon inherited colorControlNormal, which is dark and left it at
1.4:1 against the red toolbar, below the 3:1 WCAG needs for interface icons.
A toolbar-scoped overlay keeps the tint off the light screens.
The settings container carried a 25dp margin and the scrolled content another
20dp at the end, so the content sat off-centre and the outsideOverlay scrollbar
floated 25dp away from the screen edge. Move the horizontal spacing onto the
content itself and let the ScrollView span the full width.

Also drop colorSignatureDark, unused since the status bar colors went away.
The list titles were upper-cased and centred inside a weighted column that
starts after the drag handle, which left them visibly off-centre. Align them
to the start in a fixed handle column instead.

The settings action buttons sat flush against the last setting with nothing
between them; give them a divider and drop the negative bottom margin.
Two neutral text buttons are no longer a recommended pairing; make Save a
filled button and keep Cancel as a text button. The container also stacked a
25dp bottom margin on top of the gesture inset, leaving 56dp of dead space.
The light scheme matches a Material tonal-spot scheme seeded with the
signature red, but the dark one does not: its primary was #F0BE6D, an amber
from an unrelated seed, which is why the switches and buttons went amber under
the red app bar in dark mode. Only 21 of its 141 values agreed with the seed,
against 92 of 141 in the light scheme.

Regenerate both with material-color-utilities from #B71C1C so the two schemes
belong to the same family and can be reproduced:

  new SchemeTonalSpot(Hct.fromInt(argbFromHex('#B71C1C')), isDark, contrast)
The swipe backgrounds were hardcoded outside the theme: #EF6C00 orange next to
the signature red, and #FD2C2C for delete, a third red on top of the app bar
red and the scheme error red. Neither had a dark variant. Point them at
errorContainer and tertiaryContainer, which carry the same meanings and follow
the theme, and drop the unused swipe_action_positive.
One sampled colour cannot match both ends: theme-color matches the header but
not the content under the bottom bar, and the body background matches the
content but not the header. On a page with a dark header over light content
the mismatched end was off by 224 of 255 per channel. Take theme-color for the
top and the body background for the bottom, and pick each bar's icon colour
from its own side.
- restore the @SuppressLint that an insertion stranded on the wrong method
- read the page background once instead of twice, and let one place own the
  colour fallbacks instead of the JS, the Java and the Kotlin each doing it
- skip the colour sampling on the error path and in immersive mode, where it
  paints nothing
- reuse the bar drawable across navigations instead of rebuilding it
- MaterialDivider, theme button styles and colour roles instead of hand-rolled
  equivalents; move the toolbar overlay in with the other theme overlays
- keep the app bar from recolouring on scroll: Widget.Material3.AppBarLayout
  defaults to liftOnScroll with its own colour, which the Toolbar's own
  background used to hide
- never let a page crash the activity: the rgb() digits were unbounded and
  parsed outside the guard
- accept the short hex form, and read the alpha instead of matching the
  'rgba(0, 0, 0, 0' prefix, which also caught 0.85
- fall back to the theme surface when nothing parses, instead of leaving the
  previous page's colours on screen
- clear the navigation bar scrim for every screen, not only the web view
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Immersive mode - black status bar Android 15 enforces edge to edge mode

1 participant