diff --git a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/AndroidInsetsBridge.kt b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/AndroidInsetsBridge.kt index 0075f98e..3b792af6 100644 --- a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/AndroidInsetsBridge.kt +++ b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/AndroidInsetsBridge.kt @@ -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) diff --git a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/MainActivity.kt index 52f936e1..4ba7a834 100644 --- a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/MainActivity.kt @@ -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)) { diff --git a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/RustWebViewClient.kt b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/RustWebViewClient.kt index cf82495a..d4be82c6 100644 --- a/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/RustWebViewClient.kt +++ b/src-tauri/gen/android/app/src/main/java/com/tauritavern/client/RustWebViewClient.kt @@ -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") } diff --git a/tests/mobile-ime-contract.test.mjs b/tests/mobile-ime-contract.test.mjs index ac742cb8..416db925 100644 --- a/tests/mobile-ime-contract.test.mjs +++ b/tests/mobile-ime-contract.test.mjs @@ -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,