From e6b7fdd7691fd749d6c9f0c62298b3f7899eff2f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 18:16:49 +0000 Subject: [PATCH 1/3] feat: Adjust system message height and back button behavior Implements dynamic height changes for the system message text field in the PhotoReasoningScreen based on focus and keyboard visibility: - Focused + Keyboard open: 600dp - Focused + Keyboard closed: 1000dp - Not focused: 120dp Additionally, modifies back button behavior: - If the system message field is focused with the keyboard closed (1000dp height), the first back press deselects the field, changing its height to 120dp. - Subsequent back presses perform the default navigation. Keyboard visibility is detected in MainActivity and propagated to the PhotoReasoningScreen. --- .../com/google/ai/sample/MainActivity.kt | 33 +++++++++++++++++++ .../multimodal/PhotoReasoningScreen.kt | 26 ++++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt index 623cfad8..d2a6fa4a 100644 --- a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt +++ b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt @@ -11,9 +11,12 @@ import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import android.graphics.Rect import android.os.Bundle import android.provider.Settings import android.util.Log +import android.view.View +import android.view.ViewTreeObserver import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -70,6 +73,11 @@ import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { + // Keyboard Visibility + private val _isKeyboardOpen = MutableStateFlow(false) + val isKeyboardOpen: StateFlow = _isKeyboardOpen.asStateFlow() + private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null + private var photoReasoningViewModel: PhotoReasoningViewModel? = null private lateinit var apiKeyManager: ApiKeyManager private var showApiKeyDialog by mutableStateOf(false) @@ -286,6 +294,26 @@ class MainActivity : ComponentActivity() { // Initial check for accessibility service status refreshAccessibilityServiceStatus() + // Keyboard visibility listener + val rootView = findViewById(android.R.id.content) + onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + rootView.getWindowVisibleDisplayFrame(rect) + val screenHeight = rootView.rootView.height + val keypadHeight = screenHeight - rect.bottom + if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is a common threshold + if (!_isKeyboardOpen.value) { + _isKeyboardOpen.value = true + Log.d(TAG, "Keyboard visible") + } + } else { + if (_isKeyboardOpen.value) { + _isKeyboardOpen.value = false + Log.d(TAG, "Keyboard hidden") + } + } + } + rootView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) Log.d(TAG, "onCreate: Calling setContent.") setContent { @@ -752,6 +780,11 @@ class MainActivity : ComponentActivity() { billingClient.endConnection() Log.d(TAG, "onDestroy: BillingClient connection ended.") } + // Remove keyboard listener + onGlobalLayoutListener?.let { + findViewById(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(it) + Log.d(TAG, "onDestroy: Keyboard layout listener removed.") + } if (this == instance) { instance = null Log.d(TAG, "onDestroy: MainActivity instance cleared.") diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index a7a13177..d6d1e791 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -6,6 +6,7 @@ import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.provider.Settings import android.widget.Toast // Added for Toast message +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -88,6 +89,7 @@ internal fun PhotoReasoningRoute( // Observe the accessibility service status from MainActivity val isAccessibilityServiceEffectivelyEnabled by mainActivity?.isAccessibilityServiceEnabledFlow?.collectAsState() ?: mutableStateOf(false) + val isKeyboardOpen by mainActivity?.isKeyboardOpen?.collectAsState() ?: mutableStateOf(false) // Launcher for opening accessibility settings val accessibilitySettingsLauncher = rememberLauncherForActivityResult( @@ -168,7 +170,8 @@ internal fun PhotoReasoningRoute( val vm = it.getPhotoReasoningViewModel() vm?.clearChatHistory(context) } - } + }, + isKeyboardOpen = isKeyboardOpen ) } @@ -183,13 +186,19 @@ fun PhotoReasoningScreen( onReasonClicked: (String, List) -> Unit = { _, _ -> }, isAccessibilityServiceEnabled: Boolean = false, onEnableAccessibilityService: () -> Unit = {}, - onClearChatHistory: () -> Unit = {} + onClearChatHistory: () -> Unit = {}, + isKeyboardOpen: Boolean ) { var userQuestion by rememberSaveable { mutableStateOf("") } val imageUris = rememberSaveable(saver = UriSaver()) { mutableStateListOf() } + var isSystemMessageFocused by rememberSaveable { mutableStateOf(false) } val listState = rememberLazyListState() val context = LocalContext.current // Get context for Toast + BackHandler(enabled = isSystemMessageFocused && !isKeyboardOpen) { + isSystemMessageFocused = false + } + val pickMedia = rememberLauncherForActivityResult( ActivityResultContracts.PickVisualMedia() ) { uri -> @@ -223,13 +232,19 @@ fun PhotoReasoningScreen( color = MaterialTheme.colorScheme.onPrimaryContainer ) Spacer(modifier = Modifier.height(8.dp)) + val systemMessageHeight = when { + isSystemMessageFocused && isKeyboardOpen -> 600.dp + isSystemMessageFocused && !isKeyboardOpen -> 1000.dp + else -> 120.dp + } OutlinedTextField( value = systemMessage, onValueChange = onSystemMessageChanged, placeholder = { Text("Enter a system message here that will be sent with every request") }, modifier = Modifier .fillMaxWidth() - .height(120.dp), + .height(systemMessageHeight) + .onFocusChanged { focusState -> isSystemMessageFocused = focusState.isFocused }, maxLines = 5, minLines = 3 ) @@ -633,13 +648,14 @@ fun PhotoReasoningScreenPreviewWithContent() { text = "I am here to help you. What do you want to know?", participant = PhotoParticipant.MODEL ) - ) + ), + isKeyboardOpen = false ) } @Composable @Preview(showSystemUi = true) fun PhotoReasoningScreenPreviewEmpty() { - PhotoReasoningScreen() + PhotoReasoningScreen(isKeyboardOpen = false) } From a0075d8ed794eff2bd0b928919b1f9a162019f87 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 18:34:13 +0000 Subject: [PATCH 2/3] fix: Add missing import for onFocusChanged Adds the import `androidx.compose.ui.focus.onFocusChanged` to `PhotoReasoningScreen.kt` to resolve a build compilation error. --- .../google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index d6d1e791..06ccf6e9 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext From aba0f1921be2e9ccbeaba09498d16208f3c11347 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 18:54:09 +0000 Subject: [PATCH 3/3] fix: Correct system message TextField behavior Addresses your feedback on the system message TextField in PhotoReasoningScreen: - Sets focused height with keyboard to 450dp (was 600dp). - Dynamically adjusts minLines and maxLines of the OutlinedTextField to ensure the text input area expands with the component's height. - Modifies the BackHandler to explicitly clear focus from the TextField (in addition to collapsing it) when it's focused without the keyboard and back is pressed. This ensures it can be re-expanded correctly on subsequent focus. These changes improve the usability and appearance of the system message input field. --- .../feature/multimodal/PhotoReasoningScreen.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index 06ccf6e9..63365e78 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -54,6 +54,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -195,9 +196,11 @@ fun PhotoReasoningScreen( var isSystemMessageFocused by rememberSaveable { mutableStateOf(false) } val listState = rememberLazyListState() val context = LocalContext.current // Get context for Toast + val focusManager = LocalFocusManager.current BackHandler(enabled = isSystemMessageFocused && !isKeyboardOpen) { - isSystemMessageFocused = false + focusManager.clearFocus() // Clear focus first + isSystemMessageFocused = false // Then update the state that controls height } val pickMedia = rememberLauncherForActivityResult( @@ -234,10 +237,12 @@ fun PhotoReasoningScreen( ) Spacer(modifier = Modifier.height(8.dp)) val systemMessageHeight = when { - isSystemMessageFocused && isKeyboardOpen -> 600.dp + isSystemMessageFocused && isKeyboardOpen -> 450.dp // Changed from 600.dp isSystemMessageFocused && !isKeyboardOpen -> 1000.dp else -> 120.dp } + val currentMinLines = if (systemMessageHeight == 120.dp) 3 else 1 + val currentMaxLines = if (systemMessageHeight == 120.dp) 5 else Int.MAX_VALUE OutlinedTextField( value = systemMessage, onValueChange = onSystemMessageChanged, @@ -246,8 +251,8 @@ fun PhotoReasoningScreen( .fillMaxWidth() .height(systemMessageHeight) .onFocusChanged { focusState -> isSystemMessageFocused = focusState.isFocused }, - maxLines = 5, - minLines = 3 + minLines = currentMinLines, + maxLines = currentMaxLines ) } }