11package com.google.ai.sample.feature.multimodal
22
3+ // Imports bleiben größtenteils gleich, stelle sicher, dass alle benötigt werden
34import android.app.Activity
45import android.content.Context
56import android.content.Intent
6- import android.graphics.Bitmap // Keep if needed elsewhere
7+ import android.graphics.Bitmap
78import android.graphics.drawable.BitmapDrawable
89import android.media.projection.MediaProjectionManager
910import android.net.Uri
@@ -15,15 +16,18 @@ import androidx.activity.result.contract.ActivityResultContracts
1516import androidx.compose.foundation.layout.Box
1617import androidx.compose.foundation.layout.Column
1718import androidx.compose.foundation.layout.Row
19+ import androidx.compose.foundation.layout.Spacer // Import Spacer if needed
1820import androidx.compose.foundation.layout.fillMaxWidth
21+ import androidx.compose.foundation.layout.height // Import height if needed
1922import androidx.compose.foundation.layout.padding
2023import androidx.compose.foundation.layout.requiredSize
2124import androidx.compose.foundation.lazy.LazyRow
2225import androidx.compose.foundation.lazy.items
2326import androidx.compose.foundation.rememberScrollState
2427import androidx.compose.foundation.verticalScroll
2528import androidx.compose.material.icons.Icons
26- import androidx.compose.material.icons.outlined.Person // Consider changing icon for AI response
29+ import androidx.compose.material.icons.automirrored.filled.Chat // Example alternative icon
30+ import androidx.compose.material.icons.outlined.Person // Keep or change
2731import androidx.compose.material.icons.rounded.Add
2832import androidx.compose.material3.Button
2933import androidx.compose.material3.Card
@@ -57,20 +61,17 @@ import androidx.core.net.toUri
5761import androidx.lifecycle.Lifecycle
5862import androidx.lifecycle.LifecycleEventObserver
5963import androidx.lifecycle.viewmodel.compose.viewModel
60- import coil.ImageLoader // Keep if needed elsewhere
61- import coil.compose.AsyncImage
62- import coil.request.ImageRequest // Keep if needed elsewhere
63- import coil.request.SuccessResult // Keep if needed elsewhere
64- import coil.size.Precision // Keep if needed elsewhere
64+ import coil.compose.AsyncImage // Import AsyncImage
6565import com.google.ai.sample.GenerativeViewModelFactory
6666import com.google.ai.sample.R
6767import com.google.ai.sample.ScreenshotManager
6868import com.google.ai.sample.util.UriSaver // Ensure this import is correct
6969import java.io.File
7070
71+
72+ // Route Composable - Handles ViewModel, Logic, Permissions
7173@Composable
7274internal fun PhotoReasoningRoute (
73- // Use the ViewModel with the integrated logic
7475 viewModel : PhotoReasoningViewModel = viewModel(factory = GenerativeViewModelFactory )
7576) {
7677 val photoReasoningUiState by viewModel.uiState.collectAsState()
@@ -102,6 +103,7 @@ internal fun PhotoReasoningRoute(
102103 }
103104 }
104105
106+ // Call the pure UI Composable
105107 PhotoReasoningScreen (
106108 uiState = photoReasoningUiState,
107109 onRequestScreenshotPermission = {
@@ -121,30 +123,28 @@ internal fun PhotoReasoningRoute(
121123 onReasonClicked = { inputText, selectedItems ->
122124 Log .d(" PhotoReasoningRoute" , " Go button clicked. Taking screenshot." )
123125 screenshotManager.takeScreenshot { bitmap ->
124- val urisToProcess = selectedItems.toMutableList() // Start with user selected URIs
126+ val urisToProcess = selectedItems.toMutableList()
125127 if (bitmap != null ) {
126128 Log .d(" PhotoReasoningRoute" , " Screenshot successful." )
127129 val screenshotFile = screenshotManager.saveBitmapToFile(bitmap)
128130 if (screenshotFile != null ) {
129- urisToProcess.add(screenshotFile.toUri()) // Add screenshot URI if saved
131+ urisToProcess.add(screenshotFile.toUri())
130132 } else {
131133 Log .w(" PhotoReasoningRoute" , " Failed to save screenshot." )
132134 Toast .makeText(context, " Failed to save screenshot" , Toast .LENGTH_SHORT ).show()
133135 }
134- // Consider recycling the bitmap if it's large and no longer needed directly
135- // bitmap.recycle()
136136 } else {
137137 Log .e(" PhotoReasoningRoute" , " Failed to take screenshot." )
138138 Toast .makeText(context, " Failed to take screenshot" , Toast .LENGTH_SHORT ).show()
139139 }
140- // Call the ViewModel function with the final list of URIs (including screenshot if successful)
141140 viewModel.processImagesAndReason(context, inputText, urisToProcess)
142141 }
143142 }
144143 )
145144}
146145
147146
147+ // Pure UI Composable - Receives state and callbacks
148148@Composable
149149fun PhotoReasoningScreen (
150150 uiState : PhotoReasoningUiState ,
@@ -162,157 +162,170 @@ fun PhotoReasoningScreen(
162162 }
163163 }
164164
165+ // Main Column for the screen layout
165166 Column (
166167 modifier = Modifier
167168 .padding(all = 16 .dp)
168- .verticalScroll(rememberScrollState())
169+ .verticalScroll(rememberScrollState()) // Make the whole column scrollable
169170 ) {
170- // Button to request permission
171- Button (onClick = onRequestScreenshotPermission) {
171+ // --- Input Section ---
172+ Button (onClick = onRequestScreenshotPermission) { // Correct placement
172173 Text (" Grant Screenshot Permission" )
173174 }
174175
175- Card (
176- modifier = Modifier .fillMaxWidth().padding(top = 8 .dp) // Add padding
177- ) {
178- Row (
179- modifier = Modifier .padding(start = 8 .dp, end = 8 .dp, top = 16 .dp, bottom = 8 .dp) // Adjust padding
180- ) {
181- IconButton (
182- onClick = {
183- pickMedia.launch(
184- PickVisualMediaRequest (ActivityResultContracts .PickVisualMedia .ImageOnly )
185- )
186- },
187- modifier = Modifier
188- .padding(end = 4 .dp) // Adjust padding
189- .align(Alignment .CenterVertically )
190- ) {
191- Icon (
192- Icons .Rounded .Add ,
193- contentDescription = stringResource(R .string.add_image),
194- )
195- }
196- OutlinedTextField (
197- value = userQuestion,
198- label = { Text (stringResource(R .string.reason_label)) },
199- placeholder = { Text (stringResource(R .string.reason_hint)) },
200- onValueChange = { userQuestion = it },
201- modifier = Modifier
202- .weight(1f ) // Use weight
203- .padding(horizontal = 4 .dp)
204- .align(Alignment .CenterVertically ) // Align vertically
205- )
206- TextButton (
207- onClick = {
208- if (userQuestion.isNotBlank()) {
209- onReasonClicked(userQuestion, imageUris.toList())
210- } else {
211- Toast .makeText(LocalContext .current, " Please enter a question" , Toast .LENGTH_SHORT ).show()
212- }
213- },
214- modifier = Modifier
215- .padding(start = 4 .dp) // Adjust padding
216- .align(Alignment .CenterVertically )
176+ Spacer (modifier = Modifier .height(8 .dp)) // Add space
177+
178+ Card (
179+ modifier = Modifier .fillMaxWidth()
180+ ) {
181+ // Column inside the card for better structure if needed, or just Row
182+ Column {
183+ Row (
184+ modifier = Modifier .padding(start = 8 .dp, end = 8 .dp, top = 16 .dp), // Padding for the Row
185+ verticalAlignment = Alignment .CenterVertically // Align items vertically
217186 ) {
218- Text (stringResource(R .string.action_go))
219- }
220- }
221- LazyRow (
222- modifier = Modifier .padding(start = 8 .dp, end = 8 .dp, bottom = 8 .dp) // Adjust padding
223- ) {
224- items(imageUris) { imageUri ->
225- AsyncImage (
226- model = imageUri,
227- contentDescription = " Selected image" ,
187+ IconButton (
188+ onClick = {
189+ pickMedia.launch(
190+ PickVisualMediaRequest (ActivityResultContracts .PickVisualMedia .ImageOnly )
191+ )
192+ },
193+ // No extra modifier needed here if padding is on Row/Column
194+ ) {
195+ Icon (
196+ Icons .Rounded .Add ,
197+ contentDescription = stringResource(R .string.add_image),
198+ )
199+ }
200+ OutlinedTextField (
201+ value = userQuestion,
202+ label = { Text (stringResource(R .string.reason_label)) },
203+ placeholder = { Text (stringResource(R .string.reason_hint)) },
204+ onValueChange = { userQuestion = it },
228205 modifier = Modifier
229- .padding( 4 .dp)
230- .requiredSize( 72 .dp)
206+ .weight( 1f ) // Take remaining space
207+ .padding(horizontal = 8 .dp) // Padding around text field
231208 )
232- }
233- }
234- }
209+ TextButton (
210+ onClick = {
211+ if (userQuestion.isNotBlank()) {
212+ onReasonClicked(userQuestion, imageUris.toList())
213+ } else {
214+ Toast .makeText(LocalContext .current, " Please enter a question" , Toast .LENGTH_SHORT ).show()
215+ }
216+ },
217+ // No extra modifier needed here if padding is on Row/Column
218+ ) {
219+ Text (stringResource(R .string.action_go))
220+ }
221+ } // End of Row
235222
236- // Display UI State
237- when (uiState) {
223+ // Image Preview Row
224+ LazyRow (
225+ modifier = Modifier .padding(all = 8 .dp) // Padding for the LazyRow
226+ ) {
227+ items(imageUris) { imageUri ->
228+ AsyncImage ( // Correct Composable call
229+ model = imageUri,
230+ contentDescription = " Selected image" ,
231+ modifier = Modifier
232+ .padding(4 .dp)
233+ .requiredSize(72 .dp)
234+ )
235+ }
236+ } // End of LazyRow
237+ } // End of Column inside Card
238+ } // End of Card
239+
240+ Spacer (modifier = Modifier .height(16 .dp)) // Space before results
241+
242+ // --- Output Section ---
243+ // Correctly placed 'when' block directly inside the main Column
244+ when (uiState) {
238245 PhotoReasoningUiState .Initial -> {
239- Text (" Enter a question, select images, and grant permission if needed." , modifier = Modifier .padding(16 .dp))
246+ Text (
247+ text = " Enter a question, select images (optional), grant permission, and press GO." ,
248+ modifier = Modifier .padding(16 .dp),
249+ style = MaterialTheme .typography.bodyMedium // Use theme typography
250+ )
240251 }
241252
242253 PhotoReasoningUiState .Loading -> {
243254 Box (
244255 contentAlignment = Alignment .Center ,
245256 modifier = Modifier
257+ .fillMaxWidth() // Make Box take full width for centering
246258 .padding(all = 8 .dp)
247- .fillMaxWidth()
248259 ) {
249- CircularProgressIndicator ()
260+ CircularProgressIndicator () // Correct Composable call
250261 }
251262 }
252263
253264 is PhotoReasoningUiState .Success -> {
254265 Card (
255266 modifier = Modifier
256- .padding(vertical = 16 .dp)
257- .fillMaxWidth(),
267+ .fillMaxWidth(), // Card takes full width
258268 shape = MaterialTheme .shapes.large,
259269 colors = CardDefaults .cardColors(
260270 containerColor = MaterialTheme .colorScheme.secondaryContainer
261271 )
262- ) {
272+ ) { // Content lambda for Card is @Composable
263273 Row (
264274 modifier = Modifier
265275 .padding(all = 16 .dp)
266276 .fillMaxWidth()
267- ) {
277+ ) { // Content lambda for Row is @Composable
268278 Icon (
269- Icons .Outlined .Person , // TODO: Consider different icon
270- contentDescription = " AI Response Icon" ,
279+ // Icons.Outlined.Person, // Original
280+ imageVector = Icons .AutoMirrored .Filled .Chat , // Example alternative
281+ contentDescription = " AI Response" , // Better description
271282 tint = MaterialTheme .colorScheme.onSecondaryContainer,
272283 modifier = Modifier
273284 .requiredSize(36 .dp)
274- .drawBehind {
285+ .drawBehind { // drawBehind is fine here
275286 drawCircle(color = MaterialTheme .colorScheme.surface)
276287 }
277288 )
278- Text (
289+ Text ( // Correct Composable call
279290 text = uiState.outputText,
280291 color = MaterialTheme .colorScheme.onSecondaryContainer,
281292 modifier = Modifier
282293 .padding(start = 16 .dp)
283- .fillMaxWidth()
294+ .align( Alignment . CenterVertically ) // Align text with icon
284295 )
285- }
286- }
287- }
296+ } // End Row
297+ } // End Card
298+ } // End Success branch
288299
289300 is PhotoReasoningUiState .Error -> {
290301 Card (
291302 modifier = Modifier
292- .padding(vertical = 16 .dp)
293- .fillMaxWidth(),
303+ .fillMaxWidth(), // Card takes full width
294304 shape = MaterialTheme .shapes.large,
295305 colors = CardDefaults .cardColors(
296306 containerColor = MaterialTheme .colorScheme.errorContainer
297307 )
298- ) {
299- Text (
308+ ) { // Content lambda for Card is @Composable
309+ Text ( // Correct Composable call
300310 text = uiState.errorMessage,
301311 color = MaterialTheme .colorScheme.onErrorContainer,
302312 modifier = Modifier .padding(all = 16 .dp)
303313 )
304- }
305- }
306- }
307- }
314+ } // End Card
315+ } // End Error branch
316+ // 'when' is now exhaustive because all 4 states are covered
317+ } // End when
318+ } // End Main Column
308319}
309320
310321
322+ // Preview Function
311323@Preview(showSystemUi = true )
312324@Composable
313325fun PhotoReasoningScreenPreview () {
326+ // Provide dummy data for preview
314327 PhotoReasoningScreen (
315- uiState = PhotoReasoningUiState .Initial , // Start with Initial state in preview
328+ uiState = PhotoReasoningUiState .Success ( " This is a sample preview response. " ), // Example state
316329 onRequestScreenshotPermission = {},
317330 onReasonClicked = { _, _ -> }
318331 )
0 commit comments