Skip to content

Commit 019cbc2

Browse files
Add files via upload
1 parent 435775a commit 019cbc2

2 files changed

Lines changed: 159 additions & 144 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt

Lines changed: 112 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.google.ai.sample.feature.multimodal
22

3+
// Imports bleiben größtenteils gleich, stelle sicher, dass alle benötigt werden
34
import android.app.Activity
45
import android.content.Context
56
import android.content.Intent
6-
import android.graphics.Bitmap // Keep if needed elsewhere
7+
import android.graphics.Bitmap
78
import android.graphics.drawable.BitmapDrawable
89
import android.media.projection.MediaProjectionManager
910
import android.net.Uri
@@ -15,15 +16,18 @@ import androidx.activity.result.contract.ActivityResultContracts
1516
import androidx.compose.foundation.layout.Box
1617
import androidx.compose.foundation.layout.Column
1718
import androidx.compose.foundation.layout.Row
19+
import androidx.compose.foundation.layout.Spacer // Import Spacer if needed
1820
import androidx.compose.foundation.layout.fillMaxWidth
21+
import androidx.compose.foundation.layout.height // Import height if needed
1922
import androidx.compose.foundation.layout.padding
2023
import androidx.compose.foundation.layout.requiredSize
2124
import androidx.compose.foundation.lazy.LazyRow
2225
import androidx.compose.foundation.lazy.items
2326
import androidx.compose.foundation.rememberScrollState
2427
import androidx.compose.foundation.verticalScroll
2528
import 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
2731
import androidx.compose.material.icons.rounded.Add
2832
import androidx.compose.material3.Button
2933
import androidx.compose.material3.Card
@@ -57,20 +61,17 @@ import androidx.core.net.toUri
5761
import androidx.lifecycle.Lifecycle
5862
import androidx.lifecycle.LifecycleEventObserver
5963
import 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
6565
import com.google.ai.sample.GenerativeViewModelFactory
6666
import com.google.ai.sample.R
6767
import com.google.ai.sample.ScreenshotManager
6868
import com.google.ai.sample.util.UriSaver // Ensure this import is correct
6969
import java.io.File
7070

71+
72+
// Route Composable - Handles ViewModel, Logic, Permissions
7173
@Composable
7274
internal 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
149149
fun 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
313325
fun 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

Comments
 (0)