From 0df7b8bdb89bbc2b07cac1f69d6fd069a7c3315e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Ku=C5=9Bnierek?= <6025373+dkodr@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:45:40 +0200 Subject: [PATCH 01/11] Support edge-to-edge and target Android 16 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 --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 2 +- .../com/cylonid/nativealpha/AboutActivity.kt | 2 + .../com/cylonid/nativealpha/MainActivity.kt | 2 + .../cylonid/nativealpha/WebViewActivity.java | 43 +++++++++++-------- .../nativealpha/util/WindowInsetsUtils.kt | 39 +++++++++++++++++ .../nativealpha/activities/NewsActivity.kt | 2 + .../activities/ToolbarBaseActivity.kt | 2 + app/src/main/res/layout/activity_main.xml | 5 ++- .../main/res/layout/toolbar_back_action.xml | 1 + app/src/main/res/values-night/themes.xml | 4 +- app/src/main/res/values/themes.xml | 10 +++-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 14 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 27b5a6b1..411ee9b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { dependenciesInfo { includeInApk = false } - compileSdk 35 + compileSdk 36 splits { abi { enable true @@ -55,7 +55,7 @@ android { defaultConfig { applicationId "com.cylonid.nativealpha" minSdkVersion 28 - targetSdkVersion 35 + targetSdkVersion 36 versionCode 1520 versionName "1.5.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 933beb54..9d98b97b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -80,7 +80,7 @@ android:parentActivityName=".MainActivity" android:icon="@mipmap/native_alpha" android:roundIcon="@mipmap/native_alpha" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme.News"> = Build.VERSION_CODES.R) { - getWindow().setDecorFitsSystemWindows(true); WindowInsetsController controller = getWindow().getInsetsController(); if (controller != null) { controller.show(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); diff --git a/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt b/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt new file mode 100644 index 00000000..9e208980 --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt @@ -0,0 +1,39 @@ +package com.cylonid.nativealpha.util + +import android.app.Activity +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +object WindowInsetsUtils { + + private val INSET_TYPES = WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() or + WindowInsetsCompat.Type.ime() + + /** + * Pads the content view by the insets it handles and reports those as zero, so that a WebView + * further down does not pad them twice. Pass [includeTop] = false to leave the top inset to a + * fitsSystemWindows app bar drawing behind the status bar. + */ + @JvmStatic + @JvmOverloads + fun applyAsPadding(activity: Activity, includeTop: Boolean = true) { + apply(activity, INSET_TYPES, includeTop) + } + + /** For immersive mode, where only the keyboard may overlap the content. */ + @JvmStatic + fun applyKeyboardPadding(activity: Activity) { + apply(activity, WindowInsetsCompat.Type.ime(), true) + } + + private fun apply(activity: Activity, types: Int, includeTop: Boolean) { + ViewCompat.setOnApplyWindowInsetsListener(activity.findViewById(android.R.id.content)) { v, windowInsets -> + val insets = windowInsets.getInsets(types) + v.setPadding(insets.left, if (includeTop) insets.top else 0, insets.right, insets.bottom) + val unhandled = if (includeTop) Insets.NONE else Insets.of(0, insets.top, 0, 0) + WindowInsetsCompat.Builder(windowInsets).setInsets(types, unhandled).build() + } + } +} diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt b/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt index 3f645301..45d413a7 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt @@ -17,6 +17,7 @@ import com.cylonid.nativealpha.BuildConfig import com.cylonid.nativealpha.databinding.NewsActivityBinding import com.cylonid.nativealpha.model.DataManager import com.cylonid.nativealpha.util.LocaleUtils +import com.cylonid.nativealpha.util.WindowInsetsUtils class NewsActivity : AppCompatActivity(), View.OnTouchListener { @@ -38,6 +39,7 @@ class NewsActivity : AppCompatActivity(), View.OnTouchListener { binding = NewsActivityBinding.inflate(layoutInflater) val view = binding.root setContentView(view) + WindowInsetsUtils.applyAsPadding(this) binding.newsContent.settings.javaScriptEnabled = true binding.newsContent.isLongClickable = false diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt b/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt index 2c7d2f3b..13d5b68e 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding import com.cylonid.nativealpha.databinding.ActivityToolbarBaseBinding +import com.cylonid.nativealpha.util.WindowInsetsUtils abstract class ToolbarBaseActivity : AppCompatActivity() { @@ -21,6 +22,7 @@ abstract class ToolbarBaseActivity : AppCompatActivity() { val baseBinding = ActivityToolbarBaseBinding.inflate(layoutInflater) setContentView(baseBinding.root) + WindowInsetsUtils.applyAsPadding(this, false) _binding = inflateBinding(layoutInflater) baseBinding.activityContent.addView(_binding.root) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b04ab3e3..3b1867c2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,11 +4,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".MainActivity"> + android:layout_height="wrap_content" + android:background="@color/colorSignature" + android:fitsSystemWindows="true"> @color/md_theme_surfaceContainer @color/md_theme_surfaceContainerHigh @color/md_theme_surfaceContainerHighest - true - @color/colorSignatureDark + false + false diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0699d2cd..78024ebc 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -46,8 +46,8 @@ @color/md_theme_surfaceContainer @color/md_theme_surfaceContainerHigh @color/md_theme_surfaceContainerHighest - true - @color/colorSignatureDark + false + true + + diff --git a/build.gradle b/build.gradle index dd8a51a6..90d73ab4 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.8.2' + classpath 'com.android.tools.build:gradle:8.13.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:11.6.3" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0fdd28d9..cf32b223 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip From 60384666aa486648c7e21201031990a153fcf96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Ku=C5=9Bnierek?= <6025373+dkodr@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:00:36 +0200 Subject: [PATCH 02/11] Blend the system bars into the web page 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. --- .../cylonid/nativealpha/WebViewActivity.java | 7 ++++ .../nativealpha/util/WindowInsetsUtils.kt | 34 ++++++++++++++++++- app/src/main/res/values-night/themes.xml | 8 ++++- app/src/main/res/values/themes.xml | 4 ++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java b/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java index 46aa77dd..064769f4 100644 --- a/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java +++ b/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java @@ -388,6 +388,12 @@ public boolean onTouch(View v, MotionEvent event) { } @SuppressLint("RequiresFeature") + + private void matchBarsToPage() { + wv.evaluateJavascript("(function(){var m=document.querySelector('meta[name=\"theme-color\"]');if(m&&m.content)return m.content;var e=document.body||document.documentElement;while(e){var c=getComputedStyle(e).backgroundColor;if(c&&c!==\"transparent\"&&!c.startsWith(\"rgba(0, 0, 0, 0\"))return c;e=e.parentElement;}return \"\";})()", + color -> WindowInsetsUtils.applyBarColor(this, color)); + } + private void setDarkModeIfNeeded() { if (!BuildConfig.FLAVOR.contains("extended")) { return; @@ -903,6 +909,7 @@ public void onPageFinished(WebView view, String url) { wv.loadUrl("file:///android_asset/errorSite/error_" + langExtension + ".html"); } wv.evaluateJavascript("document.addEventListener(\"visibilitychange\",function (event) {event.stopImmediatePropagation();},true);", null); + matchBarsToPage(); super.onPageFinished(view, url); } diff --git a/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt b/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt index 9e208980..6dea7481 100644 --- a/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt +++ b/app/src/main/java/com/cylonid/nativealpha/util/WindowInsetsUtils.kt @@ -1,9 +1,15 @@ package com.cylonid.nativealpha.util import android.app.Activity +import android.graphics.Color +import android.os.Build +import android.view.View +import androidx.core.graphics.ColorUtils import androidx.core.graphics.Insets import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat object WindowInsetsUtils { @@ -11,6 +17,8 @@ object WindowInsetsUtils { WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.ime() + private val RGB = Regex("""rgba?\((\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,/\s]+([\d.]+))?\)""") + /** * Pads the content view by the insets it handles and reports those as zero, so that a WebView * further down does not pad them twice. Pass [includeTop] = false to leave the top inset to a @@ -22,13 +30,37 @@ object WindowInsetsUtils { apply(activity, INSET_TYPES, includeTop) } - /** For immersive mode, where only the keyboard may overlap the content. */ @JvmStatic fun applyKeyboardPadding(activity: Activity) { apply(activity, WindowInsetsCompat.Type.ime(), true) } + /** Paints the area kept free for the system bars in [cssColor] and adapts the icon colour. */ + @JvmStatic + fun applyBarColor(activity: Activity, cssColor: String?) { + val color = parseCssColor(cssColor) ?: return + activity.findViewById(android.R.id.content).setBackgroundColor(color) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + val light = ColorUtils.calculateLuminance(color) > 0.5 + WindowInsetsControllerCompat(activity.window, activity.window.decorView).apply { + isAppearanceLightStatusBars = light + isAppearanceLightNavigationBars = light + } + } + + private fun parseCssColor(value: String?): Int? { + val v = value?.trim()?.removeSurrounding("\"")?.trim()?.takeIf { it.isNotEmpty() } ?: return null + RGB.matchEntire(v)?.let { m -> + if ((m.groupValues[4].toFloatOrNull() ?: 1f) < 0.5f) return null + return Color.rgb(m.groupValues[1].toInt(), m.groupValues[2].toInt(), m.groupValues[3].toInt()) + } + return runCatching { Color.parseColor(v) }.getOrNull() + } + private fun apply(activity: Activity, types: Int, includeTop: Boolean) { + WindowCompat.setDecorFitsSystemWindows(activity.window, false) ViewCompat.setOnApplyWindowInsetsListener(activity.findViewById(android.R.id.content)) { v, windowInsets -> val insets = windowInsets.getInsets(types) v.setPadding(insets.left, if (includeTop) insets.top else 0, insets.right, insets.bottom) diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 9d7bf40d..05d8d109 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ - + + + From 14848dad945316025babc566a1b36838abd182ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Ku=C5=9Bnierek?= <6025373+dkodr@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:03:39 +0200 Subject: [PATCH 03/11] Tint the toolbar icons for the signature background 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. --- app/src/main/res/layout/activity_main.xml | 3 ++- app/src/main/res/layout/toolbar_back_action.xml | 2 +- app/src/main/res/values/themes.xml | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3b1867c2..4b3ac749 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -19,7 +19,8 @@ app:titleTextColor="@android:color/white" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/colorSignature" /> + android:background="@color/colorSignature" + android:theme="@style/ThemeOverlay.AppTheme.Toolbar" /> diff --git a/app/src/main/res/layout/toolbar_back_action.xml b/app/src/main/res/layout/toolbar_back_action.xml index 50079a2e..caa3f64b 100644 --- a/app/src/main/res/layout/toolbar_back_action.xml +++ b/app/src/main/res/layout/toolbar_back_action.xml @@ -6,7 +6,7 @@ android:layout_height="wrap_content" android:background="@color/colorSignature" android:fitsSystemWindows="true" - android:theme="@style/ThemeOverlay.Material3.ActionBar" + android:theme="@style/ThemeOverlay.AppTheme.Toolbar" app:titleTextColor="@android:color/white" app:navigationIconTint="@android:color/white" app:navigationIcon="@drawable/ic_baseline_arrow_back_24"/> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 40d44825..e34ee382 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -52,6 +52,10 @@ true + + From 2c00511da81d5944a0723084a71e95979998f298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Ku=C5=9Bnierek?= <6025373+dkodr@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:06:06 +0200 Subject: [PATCH 04/11] Even out the settings margins and free the scrollbar 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. --- app/src/main/res/layout/global_settings.xml | 7 +++++-- app/src/main/res/layout/webapp_settings.xml | 9 +++++++-- app/src/main/res/values/colors.xml | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/global_settings.xml b/app/src/main/res/layout/global_settings.xml index 421d50bd..530f8bee 100644 --- a/app/src/main/res/layout/global_settings.xml +++ b/app/src/main/res/layout/global_settings.xml @@ -18,7 +18,8 @@ @@ -33,7 +34,8 @@ @@ -170,6 +172,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" + android:layout_marginEnd="@dimen/margin_activity" android:layout_marginBottom="-10dp" android:orientation="horizontal"> diff --git a/app/src/main/res/layout/webapp_settings.xml b/app/src/main/res/layout/webapp_settings.xml index ef1a7c58..5225575b 100644 --- a/app/src/main/res/layout/webapp_settings.xml +++ b/app/src/main/res/layout/webapp_settings.xml @@ -21,7 +21,8 @@ @@ -29,6 +30,8 @@ android:id="@+id/labelTitle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/margin_activity" + android:layout_marginEnd="@dimen/margin_activity" android:text="@{webapp.baseUrl}" android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> @@ -47,7 +50,8 @@ @@ -667,6 +671,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" + android:layout_marginEnd="@dimen/margin_activity" android:orientation="horizontal">