diff --git a/android/app/src/main/java/io/picoclaw/android/assistant/AssistantService.kt b/android/app/src/main/java/io/picoclaw/android/assistant/AssistantService.kt index 2b31e67f7..00adb63d4 100644 --- a/android/app/src/main/java/io/picoclaw/android/assistant/AssistantService.kt +++ b/android/app/src/main/java/io/picoclaw/android/assistant/AssistantService.kt @@ -87,6 +87,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner { private lateinit var screenCaptureManager: ScreenCaptureManager private var showAccessibilityGuide by mutableStateOf(false) + private var overlayAtTop by mutableStateOf(false) private var overlayView: View? = null private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager } @@ -224,6 +225,14 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner { stopSelf() } + private fun moveOverlayTo(top: Boolean) { + val view = overlayView ?: return + val lp = view.layoutParams as? WindowManager.LayoutParams ?: return + lp.gravity = if (top) Gravity.TOP else Gravity.BOTTOM + windowManager.updateViewLayout(view, lp) + overlayAtTop = top + } + private fun setOverlayVisible(visible: Boolean) { val view = overlayView ?: return val lp = view.layoutParams as? WindowManager.LayoutParams ?: return @@ -252,15 +261,16 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner { WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT ).apply { - gravity = Gravity.BOTTOM + gravity = if (overlayAtTop) Gravity.TOP else Gravity.BOTTOM } val wrapper = object : FrameLayout(this@AssistantService) { @Volatile var contentTop = fixedHeightPx + @Volatile var contentBottom = Int.MAX_VALUE private var gestureInContent = false override fun dispatchTouchEvent(ev: MotionEvent): Boolean { if (ev.actionMasked == MotionEvent.ACTION_DOWN) { - gestureInContent = ev.y >= contentTop + gestureInContent = ev.y >= contentTop && ev.y <= contentBottom } if (!gestureInContent) return false return super.dispatchTouchEvent(ev) @@ -276,17 +286,20 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner { val state by assistantManager.state.collectAsState() Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter + contentAlignment = if (overlayAtTop) Alignment.TopCenter else Alignment.BottomCenter ) { AssistantPillBar( state = state, + isAtTop = overlayAtTop, onClose = { shutdown() }, onInterrupt = { assistantManager.interrupt() }, + onPositionChange = { top -> moveOverlayTo(top) }, onCameraToggle = { handleCameraToggle() }, onScreenCaptureToggle = { handleScreenCaptureToggle() }, cameraCaptureManager = cameraCaptureManager, modifier = Modifier.onGloballyPositioned { coordinates -> wrapper.contentTop = coordinates.positionInWindow().y.toInt() + wrapper.contentBottom = (coordinates.positionInWindow().y + coordinates.size.height).toInt() } ) } diff --git a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantPillBar.kt b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantPillBar.kt index 58b27a8dc..c5ba990df 100644 --- a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantPillBar.kt +++ b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantPillBar.kt @@ -73,8 +73,10 @@ private val ErrorColor = Color(0xFFEF4444) @Composable fun AssistantPillBar( state: VoiceModeState, + isAtTop: Boolean, onClose: () -> Unit, onInterrupt: () -> Unit, + onPositionChange: (Boolean) -> Unit, onCameraToggle: () -> Unit, onScreenCaptureToggle: () -> Unit, cameraCaptureManager: CameraCaptureManager, @@ -97,8 +99,8 @@ fun AssistantPillBar( // Camera preview above the pill bar AnimatedVisibility( visible = state.isCameraActive, - enter = expandVertically(expandFrom = Alignment.Bottom), - exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + enter = expandVertically(expandFrom = if (isAtTop) Alignment.Top else Alignment.Bottom), + exit = shrinkVertically(shrinkTowards = if (isAtTop) Alignment.Top else Alignment.Bottom) ) { val lifecycleOwner = LocalLifecycleOwner.current @@ -286,6 +288,22 @@ fun AssistantPillBar( ) } + // Move position toggle + IconButton( + onClick = { onPositionChange(!isAtTop) }, + modifier = Modifier.size(36.dp) + ) { + Icon( + painter = painterResource( + if (isAtTop) LucideR.drawable.lucide_ic_chevron_down + else LucideR.drawable.lucide_ic_chevron_up + ), + contentDescription = if (isAtTop) "Move to bottom" else "Move to top", + modifier = Modifier.size(18.dp), + tint = TextSecondary + ) + } + // Close button IconButton( onClick = onClose,