fix (Android): Fixed layout contract injection failure on Android after a reload.

This commit is contained in:
Darkatse 2026-04-03 17:45:44 -04:00
parent 4b6de827be
commit 1e6f209919
4 changed files with 69 additions and 3 deletions

View file

@ -50,9 +50,12 @@ class AndroidInsetsBridge(
}
fun onWebViewAvailable() {
lastPushedInsetsSnapshot = null
hasReadyPageInsetsInjection = false
webViewInsetsStyleApplier.onWebViewContextReset()
resetWebViewInjectionState()
refreshInjection()
}
fun onMainFrameNavigationStarted() {
resetWebViewInjectionState()
refreshInjection()
}
@ -75,6 +78,13 @@ class AndroidInsetsBridge(
scheduleInsetsSyncWhenPageReady()
}
private fun resetWebViewInjectionState() {
lastPushedInsetsSnapshot = null
hasPendingForcedInsetsPush = false
hasReadyPageInsetsInjection = false
webViewInsetsStyleApplier.onWebViewContextReset()
}
@Suppress("DEPRECATION")
private fun configureImmersiveSystemBars() {
WindowCompat.setDecorFitsSystemWindows(window, false)

View file

@ -95,6 +95,7 @@ class MainActivity : TauriActivity(), AndroidWebFullscreenHost {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
installWebViewNavigationHooks()
backNavigationController.register(onBackPressedDispatcher, this)
// Keep a foreground service for the whole app session to reduce OEM background kills.
aiGenerationNotifier.ensureKeepAliveService()
@ -142,9 +143,23 @@ class MainActivity : TauriActivity(), AndroidWebFullscreenHost {
isActivityDestroyed = true
mainHandler.removeCallbacksAndMessages(null)
backgroundExecutor.shutdownNow()
RustWebViewClient.mainFrameNavigationListener = null
super.onDestroy()
}
private fun installWebViewNavigationHooks() {
RustWebViewClient.mainFrameNavigationListener =
object : RustWebViewClient.MainFrameNavigationListener {
override fun onMainFramePageStarted(view: WebView, url: String) {
val activeWebView = webView ?: return
if (view !== activeWebView) {
return
}
insetsBridge.onMainFrameNavigationStarted()
}
}
}
private fun captureShareIntent(intent: Intent?) {
val incomingIntent = intent ?: return
if (!shareIntentParser.canHandle(incomingIntent)) {

View file

@ -25,6 +25,10 @@ class RustWebViewClient(context: Context) : WebViewClient() {
private var lastInterceptedUrl: Uri? = null
private var pendingUrlRedirect: String? = null
interface MainFrameNavigationListener {
fun onMainFramePageStarted(view: WebView, url: String)
}
private val assetLoader =
WebViewAssetLoader
.Builder()
@ -75,6 +79,7 @@ class RustWebViewClient(context: Context) : WebViewClient() {
favicon: Bitmap?,
) {
currentUrl = url
mainFrameNavigationListener?.onMainFramePageStarted(view, url)
if (interceptedState[url] == false) {
val webView = view as RustWebView
for (script in webView.initScripts) {
@ -111,6 +116,9 @@ class RustWebViewClient(context: Context) : WebViewClient() {
companion object {
private const val LOG_TAG = "TauriTavern/WebView"
@Volatile
var mainFrameNavigationListener: MainFrameNavigationListener? = null
init {
System.loadLibrary("tauritavern_lib")
}

View file

@ -250,6 +250,39 @@ test('Android insets helper exposes routing APIs and clears old IME target resid
assert.match(source, /document\.getElementById\('sheld'\)/);
});
test('Android insets injection resets on main-frame navigation', async () => {
const clientPath = path.join(
REPO_ROOT,
'src-tauri/gen/android/app/src/main/java/com/tauritavern/client/RustWebViewClient.kt',
);
const clientSource = await readFile(clientPath, 'utf8');
assert.match(clientSource, /\bmainFrameNavigationListener\b/);
assert.match(clientSource, /onMainFramePageStarted/);
assert.match(clientSource, /mainFrameNavigationListener\?\.\s*onMainFramePageStarted\(/);
const activityPath = path.join(
REPO_ROOT,
'src-tauri/gen/android/app/src/main/java/com/tauritavern/client/MainActivity.kt',
);
const activitySource = await readFile(activityPath, 'utf8');
assert.match(activitySource, /installWebViewNavigationHooks/);
assert.match(activitySource, /RustWebViewClient\.mainFrameNavigationListener\s*=/);
assert.match(activitySource, /insetsBridge\.onMainFrameNavigationStarted\(\)/);
assert.match(activitySource, /RustWebViewClient\.mainFrameNavigationListener\s*=\s*null/);
const bridgePath = path.join(
REPO_ROOT,
'src-tauri/gen/android/app/src/main/java/com/tauritavern/client/AndroidInsetsBridge.kt',
);
const bridgeSource = await readFile(bridgePath, 'utf8');
assert.match(bridgeSource, /fun onMainFrameNavigationStarted\(\)/);
assert.match(bridgeSource, /hasReadyPageInsetsInjection\s*=\s*false/);
assert.match(bridgeSource, /webViewInsetsStyleApplier\.onWebViewContextReset\(\)/);
});
test('mobile IME surface controller is focus-driven (no observers) and writes host-private attrs', async () => {
const controllerPath = path.join(
REPO_ROOT,