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:
@@ -60,19 +60,6 @@ class CyreneVoiceInteractionSession(context: Context) :
|
|||||||
RuntimeLog.general("overlay", "ViewModel unavailable, overlay static")
|
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
|
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
||||||
val vm = overlayViewModel
|
val vm = overlayViewModel
|
||||||
return ComposeView(context).apply {
|
return ComposeView(context).apply {
|
||||||
@@ -102,6 +89,9 @@ class CyreneVoiceInteractionSession(context: Context) :
|
|||||||
RuntimeLog.general("overlay", "onShow, vm=${overlayViewModel != null}")
|
RuntimeLog.general("overlay", "onShow, vm=${overlayViewModel != null}")
|
||||||
lifecycleRegistry.currentState = Lifecycle.State.STARTED
|
lifecycleRegistry.currentState = Lifecycle.State.STARTED
|
||||||
|
|
||||||
|
// Configure window: extend behind status bar, don't resize for IME
|
||||||
|
configureWindow()
|
||||||
|
|
||||||
val screenContent = CyreneAccessibilityService.getScreenContent()
|
val screenContent = CyreneAccessibilityService.getScreenContent()
|
||||||
if (screenContent.isNotBlank()) {
|
if (screenContent.isNotBlank()) {
|
||||||
overlayViewModel?.sendScreenContext(screenContent)
|
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() {
|
override fun onHide() {
|
||||||
RuntimeLog.general("overlay", "onHide")
|
RuntimeLog.general("overlay", "onHide")
|
||||||
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.draw.clip
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -131,6 +131,25 @@ fun OverlayContent(
|
|||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
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) {
|
LaunchedEffect(messages.size) {
|
||||||
if (messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
listState.animateScrollToItem(messages.size - 1)
|
listState.animateScrollToItem(messages.size - 1)
|
||||||
@@ -152,8 +171,7 @@ fun OverlayContent(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.statusBarsPadding()
|
.padding(top = statusBarPaddingDp, bottom = navBarPaddingDp),
|
||||||
.navigationBarsPadding(),
|
|
||||||
) {
|
) {
|
||||||
if (isLandscape) {
|
if (isLandscape) {
|
||||||
LandscapeContent(
|
LandscapeContent(
|
||||||
|
|||||||
Reference in New Issue
Block a user