fix: overlay status bar coverage and IME input flying in portrait mode

- Restore FLAG_TRANSLUCENT_STATUS and FLAG_TRANSLUCENT_NAVIGATION on
  VoiceInteractionSession window to let content extend behind system bars
- Move window configuration from onCreateContentView to onShow (window
  is guaranteed available at this point)
- Replace statusBarsPadding/navigationBarsPadding with manual status bar
  height calculation — Compose WindowInsets may not receive proper values
  in VoiceInteractionSession overlay windows
- Keep SOFT_INPUT_ADJUST_NOTHING + imePadding on InputArea for correct
  IME behavior (full-screen IME pushes input, floating IME does not)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:01:58 +08:00
parent 367ef7f2d6
commit 2725fdd1d5
2 changed files with 39 additions and 17 deletions
@@ -60,19 +60,6 @@ class CyreneVoiceInteractionSession(context: Context) :
RuntimeLog.general("overlay", "ViewModel unavailable, overlay static")
}
// Configure window: prevent IME resize, don't cover status bar
try {
val method = VoiceInteractionSession::class.java.getDeclaredMethod("getWindow")
method.isAccessible = true
val w = method.invoke(this) as? android.view.Window
w?.apply {
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
} catch (_: Exception) { }
lifecycleRegistry.currentState = Lifecycle.State.CREATED
val vm = overlayViewModel
return ComposeView(context).apply {
@@ -102,6 +89,9 @@ class CyreneVoiceInteractionSession(context: Context) :
RuntimeLog.general("overlay", "onShow, vm=${overlayViewModel != null}")
lifecycleRegistry.currentState = Lifecycle.State.STARTED
// Configure window: extend behind status bar, don't resize for IME
configureWindow()
val screenContent = CyreneAccessibilityService.getScreenContent()
if (screenContent.isNotBlank()) {
overlayViewModel?.sendScreenContext(screenContent)
@@ -109,6 +99,20 @@ class CyreneVoiceInteractionSession(context: Context) :
}
}
private fun configureWindow() {
try {
val method = VoiceInteractionSession::class.java.getDeclaredMethod("getWindow")
method.isAccessible = true
val w = method.invoke(this) as? android.view.Window ?: return
w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
Log.d(TAG, "Window configured: translucent status/nav, adjust nothing for IME")
} catch (e: Exception) {
Log.w(TAG, "Failed to configure window: ${e.message}")
}
}
override fun onHide() {
RuntimeLog.general("overlay", "onHide")
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
@@ -18,11 +18,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -54,6 +52,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -131,6 +131,25 @@ fun OverlayContent(
val configuration = LocalConfiguration.current
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
// Manual status bar height — Compose WindowInsets may not work in VoiceInteractionSession
val context = androidx.compose.ui.platform.LocalContext.current
val statusBarHeight = remember {
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) context.resources.getDimensionPixelSize(resourceId) else 0
}
val statusBarPaddingDp = with(androidx.compose.ui.platform.LocalDensity.current) {
statusBarHeight.toDp()
}
// Manual nav bar height
val navBarHeight = remember {
val resourceId = context.resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) context.resources.getDimensionPixelSize(resourceId) else 0
}
val navBarPaddingDp = with(androidx.compose.ui.platform.LocalDensity.current) {
navBarHeight.toDp()
}
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) {
listState.animateScrollToItem(messages.size - 1)
@@ -152,8 +171,7 @@ fun OverlayContent(
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding(),
.padding(top = statusBarPaddingDp, bottom = navBarPaddingDp),
) {
if (isLandscape) {
LandscapeContent(