11package com.google.ai.sample.feature.multimodal
22
3+ import android.app.Activity // Import Activity if not already present
34import android.graphics.drawable.BitmapDrawable
45import android.net.Uri
56import androidx.activity.compose.rememberLauncherForActivityResult
@@ -55,99 +56,158 @@ import com.google.ai.sample.ScreenshotManager
5556import com.google.ai.sample.util.UriSaver
5657import kotlinx.coroutines.launch
5758import android.content.Context
59+ import android.content.Intent // Import Intent
5860import android.graphics.Bitmap
5961import android.widget.Toast
62+ import androidx.compose.runtime.DisposableEffect
63+ import androidx.compose.ui.platform.LocalLifecycleOwner
6064import androidx.core.net.toUri
65+ import androidx.lifecycle.Lifecycle
66+ import androidx.lifecycle.LifecycleEventObserver
6167import java.io.File
6268
69+
6370@Composable
6471internal fun PhotoReasoningRoute (
6572 viewModel : PhotoReasoningViewModel = viewModel(factory = GenerativeViewModelFactory )
6673) {
6774 val photoReasoningUiState by viewModel.uiState.collectAsState()
68-
69- val coroutineScope = rememberCoroutineScope()
70- val imageRequestBuilder = ImageRequest .Builder (LocalContext .current)
71- val imageLoader = ImageLoader .Builder (LocalContext .current).build()
7275 val context = LocalContext .current
76+ val lifecycleOwner = LocalLifecycleOwner .current
77+
78+ // Use ScreenshotManager instance
79+ val screenshotManager = rememberSaveable(saver = ScreenshotManagerSaver (context)) {
80+ ScreenshotManager .getInstance(context)
81+ }
82+
83+
84+ // Activity Result Launcher for Media Projection Permission
85+ val mediaProjectionLauncher = rememberLauncherForActivityResult(
86+ contract = ActivityResultContracts .StartActivityForResult ()
87+ ) { result ->
88+ Log .d(" PhotoReasoningRoute" , " MediaProjectionLauncher result: ${result.resultCode} " )
89+ screenshotManager.handlePermissionResult(result.resultCode, result.data)
90+ // Optional: Trigger screenshot immediately after permission if needed
91+ // if (screenshotManager.handlePermissionResult(result.resultCode, result.data)) {
92+ // // Trigger screenshot or enable button etc.
93+ // }
94+ }
95+
96+ // Clean up ScreenshotManager when the composable leaves the screen
97+ DisposableEffect (lifecycleOwner) {
98+ val observer = LifecycleEventObserver { _, event ->
99+ if (event == Lifecycle .Event .ON_DESTROY ) {
100+ Log .d(" PhotoReasoningRoute" , " Lifecycle ON_DESTROY, releasing ScreenshotManager" )
101+ screenshotManager.release()
102+ }
103+ }
104+ lifecycleOwner.lifecycle.addObserver(observer)
105+ onDispose {
106+ Log .d(" PhotoReasoningRoute" , " Composable onDispose" )
107+ lifecycleOwner.lifecycle.removeObserver(observer)
108+ // Consider if release() should happen here or strictly on ON_DESTROY
109+ // screenshotManager.release() // Might be too early if composable recomposes
110+ }
111+ }
112+
73113
74114 PhotoReasoningScreen (
75115 uiState = photoReasoningUiState,
76- onReasonClicked = { inputText, selectedItems ->
77- coroutineScope.launch {
78- // Take screenshot when Go button is pressed
79- val screenshotManager = ScreenshotManager .getInstance(context)
80-
81- // Use a callback approach with non-blocking behavior
82- screenshotManager.takeScreenshot { bitmap ->
83- if (bitmap != null ) {
84- // Save screenshot to file
85- val screenshotFile = screenshotManager.saveBitmapToFile(bitmap)
86- if (screenshotFile != null ) {
87- // Add screenshot URI to selected items
88- val updatedItems = selectedItems.toMutableList()
89- updatedItems.add(screenshotFile.toUri())
90-
91- // Process all images including screenshot within the coroutine scope
92- coroutineScope.launch {
93- processImagesAndReason(updatedItems, inputText, imageRequestBuilder, imageLoader, viewModel)
94- }
95- } else {
96- // If screenshot saving failed, proceed with original images
97- coroutineScope.launch {
98- processImagesAndReason(selectedItems, inputText, imageRequestBuilder, imageLoader, viewModel)
99- }
100- Toast .makeText(context, " Failed to save screenshot" , Toast .LENGTH_SHORT ).show()
101- }
102- } else {
103- // If screenshot failed, proceed with original images
104- coroutineScope.launch {
105- processImagesAndReason(selectedItems, inputText, imageRequestBuilder, imageLoader, viewModel)
106- }
107- Toast .makeText(context, " Failed to take screenshot" , Toast .LENGTH_SHORT ).show()
108- }
109- }
116+ onRequestScreenshotPermission = {
117+ try {
118+ val mediaProjectionManager = context.getSystemService(Context .MEDIA_PROJECTION_SERVICE ) as MediaProjectionManager
119+ mediaProjectionLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
120+ } catch (e: Exception ) {
121+ Log .e(" PhotoReasoningRoute" , " Error requesting screenshot permission: ${e.message} " , e)
122+ Toast .makeText(context, " Could not request screen capture." , Toast .LENGTH_SHORT ).show()
110123 }
124+ },
125+ onReasonClicked = { inputText, selectedItems ->
126+ Log .d(" PhotoReasoningRoute" , " Go button clicked. Taking screenshot." )
127+ // Take screenshot using the manager
128+ screenshotManager.takeScreenshot { bitmap ->
129+ if (bitmap != null ) {
130+ Log .d(" PhotoReasoningRoute" , " Screenshot successful." )
131+ val screenshotFile = screenshotManager.saveBitmapToFile(bitmap)
132+ if (screenshotFile != null ) {
133+ val updatedItems = selectedItems.toMutableList()
134+ updatedItems.add(screenshotFile.toUri())
135+ // Process images including the screenshot
136+ viewModel.processImagesAndReason(context, inputText, updatedItems) // Pass context
137+ } else {
138+ Log .w(" PhotoReasoningRoute" , " Failed to save screenshot, proceeding without it." )
139+ Toast .makeText(context, " Failed to save screenshot" , Toast .LENGTH_SHORT ).show()
140+ // Process only the originally selected images
141+ viewModel.processImagesAndReason(context, inputText, selectedItems) // Pass context
142+ }
143+ // Recycle the bitmap if you are done with it and didn't pass it elsewhere
144+ // bitmap.recycle() // Be careful with recycling if the bitmap is used asynchronously
145+ } else {
146+ Log .e(" PhotoReasoningRoute" , " Failed to take screenshot, proceeding without it." )
147+ Toast .makeText(context, " Failed to take screenshot" , Toast .LENGTH_SHORT ).show()
148+ // Process only the originally selected images
149+ viewModel.processImagesAndReason(context, inputText, selectedItems) // Pass context
150+ }
151+ }
111152 }
112153 )
113154}
114155
115- // Helper function to process images and call reason
116- private suspend fun processImagesAndReason (
117- selectedItems : List <Uri >,
156+ // Add this Saver for ScreenshotManager if needed for process death restoration,
157+ // though typically you might re-initialize based on context.
158+ // Note: This doesn't save the internal state like resultCode/resultData.
159+ // Proper state saving for MediaProjection across process death is complex.
160+ fun ScreenshotManagerSaver (context : Context ) = androidx.compose.runtime.saveable.Saver <ScreenshotManager , Boolean >(
161+ save = { true }, // Just save a placeholder
162+ restore = { ScreenshotManager .getInstance(context) } // Restore by getting instance
163+ )
164+
165+
166+ // Modify ViewModel to handle image processing
167+ // Add a function like this to your PhotoReasoningViewModel
168+ fun PhotoReasoningViewModel.processImagesAndReason (
169+ context : Context , // Pass context for ImageLoader
118170 inputText : String ,
119- imageRequestBuilder : ImageRequest .Builder ,
120- imageLoader : ImageLoader ,
121- viewModel : PhotoReasoningViewModel
171+ selectedItems : List <Uri >
122172) {
123- val bitmaps = selectedItems.mapNotNull {
124- val imageRequest = imageRequestBuilder
125- .data(it)
126- // Scale the image down to 768px for faster uploads deaktiviert um genaue Auflösungen feedback zu bekommen
127- // .size(size = 768)
128- .precision(Precision .EXACT )
129- .build()
130- try {
131- val result = imageLoader.execute(imageRequest)
132- if (result is SuccessResult ) {
133- return @mapNotNull (result.drawable as BitmapDrawable ).bitmap
134- } else {
135- return @mapNotNull null
173+ viewModelScope.launch { // Use viewModelScope
174+ val imageRequestBuilder = ImageRequest .Builder (context)
175+ val imageLoader = ImageLoader .Builder (context).build()
176+
177+ val bitmaps = selectedItems.mapNotNull { uri ->
178+ val imageRequest = imageRequestBuilder
179+ .data(uri)
180+ .precision(Precision .EXACT ) // Use EXACT as per your original code
181+ .allowHardware(false ) // Disable hardware bitmaps for better compatibility if needed
182+ .build()
183+ try {
184+ val result = imageLoader.execute(imageRequest)
185+ if (result is SuccessResult ) {
186+ (result.drawable as ? BitmapDrawable )?.bitmap
187+ } else {
188+ Log .w(" PhotoReasoningViewModel" , " Failed to load image: $uri " )
189+ null
190+ }
191+ } catch (e: Exception ) {
192+ Log .e(" PhotoReasoningViewModel" , " Error loading image $uri : ${e.message} " , e)
193+ null
136194 }
137- } catch (e: Exception ) {
138- return @mapNotNull null
139195 }
196+ Log .d(" PhotoReasoningViewModel" , " Processed ${bitmaps.size} bitmaps out of ${selectedItems.size} URIs." )
197+ // Call the original reason function
198+ reason(inputText, bitmaps)
140199 }
141- viewModel.reason(inputText, bitmaps)
142200}
143201
202+
144203@Composable
145204fun PhotoReasoningScreen (
146205 uiState : PhotoReasoningUiState = PhotoReasoningUiState .Loading ,
206+ onRequestScreenshotPermission : () -> Unit = {}, // Callback to request permission
147207 onReasonClicked : (String , List <Uri >) -> Unit = { _, _ -> }
148208) {
149209 var userQuestion by rememberSaveable { mutableStateOf(" " ) }
150- val imageUris = rememberSaveable(saver = UriSaver ()) { mutableStateListOf() }
210+ val imageUris = rememberSaveable(saver = UriSaver ()) { mutableStateListOf< Uri > () } // Ensure type
151211
152212 val pickMedia = rememberLauncherForActivityResult(
153213 ActivityResultContracts .PickVisualMedia ()
@@ -157,11 +217,20 @@ fun PhotoReasoningScreen(
157217 }
158218 }
159219
220+ // TODO: Add a button or mechanism to trigger onRequestScreenshotPermission()
221+ // For example, you could add a dedicated "Request Permission" button,
222+ // or call it the first time the user tries to take a screenshot if permission wasn't granted.
223+
160224 Column (
161225 modifier = Modifier
162226 .padding(all = 16 .dp)
163227 .verticalScroll(rememberScrollState())
164228 ) {
229+ // Example Button to request permission (place appropriately in your UI)
230+ Button (onClick = onRequestScreenshotPermission) {
231+ Text (" Grant Screenshot Permission" )
232+ }
233+
165234 Card (
166235 modifier = Modifier .fillMaxWidth()
167236 ) {
@@ -189,12 +258,16 @@ fun PhotoReasoningScreen(
189258 placeholder = { Text (stringResource(R .string.reason_hint)) },
190259 onValueChange = { userQuestion = it },
191260 modifier = Modifier
192- .fillMaxWidth(0.8f )
261+ .fillMaxWidth(0.8f ) // Adjust weight/fillMaxWidth as needed
193262 )
194263 TextButton (
195264 onClick = {
196265 if (userQuestion.isNotBlank()) {
266+ // This now triggers the flow including the screenshot attempt
197267 onReasonClicked(userQuestion, imageUris.toList())
268+ } else {
269+ // Optional: Show a message if the question is blank
270+ // Toast.makeText(LocalContext.current, "Please enter a question", Toast.LENGTH_SHORT).show()
198271 }
199272 },
200273 modifier = Modifier
@@ -210,89 +283,50 @@ fun PhotoReasoningScreen(
210283 items(imageUris) { imageUri ->
211284 AsyncImage (
212285 model = imageUri,
213- contentDescription = null ,
286+ contentDescription = " Selected image " , // Add content description
214287 modifier = Modifier
215288 .padding(4 .dp)
216289 .requiredSize(72 .dp)
217290 )
218291 }
219292 }
220293 }
221- when (uiState) {
222- PhotoReasoningUiState .Initial -> {
223- // Nothing is shown
224- }
225-
226- PhotoReasoningUiState .Loading -> {
227- Box (
228- contentAlignment = Alignment .Center ,
229- modifier = Modifier
230- .padding(all = 8 .dp)
231- .align(Alignment .CenterHorizontally )
232- ) {
233- CircularProgressIndicator ()
234- }
235- }
236-
237- is PhotoReasoningUiState .Success -> {
238- Card (
239- modifier = Modifier
240- .padding(vertical = 16 .dp)
241- .fillMaxWidth(),
242- shape = MaterialTheme .shapes.large,
243- colors = CardDefaults .cardColors(
244- containerColor = MaterialTheme .colorScheme.onSecondaryContainer
245- )
246- ) {
247- Row (
248- modifier = Modifier
249- .padding(all = 16 .dp)
250- .fillMaxWidth()
251- ) {
252- Icon (
253- Icons .Outlined .Person ,
254- contentDescription = " Person Icon" ,
255- tint = MaterialTheme .colorScheme.onSecondary,
256- modifier = Modifier
257- .requiredSize(36 .dp)
258- .drawBehind {
259- drawCircle(color = Color .White )
260- }
261- )
262- Text (
263- text = uiState.outputText, // TODO(thatfiredev): Figure out Markdown support
264- color = MaterialTheme .colorScheme.onSecondary,
265- modifier = Modifier
266- .padding(start = 16 .dp)
267- .fillMaxWidth()
268- )
269- }
270- }
271- }
272-
273- is PhotoReasoningUiState .Error -> {
274- Card (
275- modifier = Modifier
276- .padding(vertical = 16 .dp)
277- .fillMaxWidth(),
278- shape = MaterialTheme .shapes.large,
279- colors = CardDefaults .cardColors(
280- containerColor = MaterialTheme .colorScheme.errorContainer
281- )
282- ) {
283- Text (
284- text = uiState.errorMessage,
285- color = MaterialTheme .colorScheme.error,
286- modifier = Modifier .padding(all = 16 .dp)
287- )
288- }
289- }
294+ // Rest of the UI remains the same (Initial, Loading, Success, Error states)
295+ when (uiState) {
296+ PhotoReasoningUiState .Initial -> { /* ... */ }
297+ PhotoReasoningUiState .Loading -> { /* ... */ }
298+ is PhotoReasoningUiState .Success -> { /* ... */ }
299+ is PhotoReasoningUiState .Error -> { /* ... */ }
290300 }
291301 }
292302}
293303
294- @Composable
304+
305+ // Dummy UI State and Preview if needed
306+ sealed interface PhotoReasoningUiState {
307+ object Initial : PhotoReasoningUiState
308+ object Loading : PhotoReasoningUiState
309+ data class Success (val outputText : String ) : PhotoReasoningUiState
310+ data class Error (val errorMessage : String ) : PhotoReasoningUiState
311+ }
312+
295313@Preview(showSystemUi = true )
314+ @Composable
296315fun PhotoReasoningScreenPreview () {
297- PhotoReasoningScreen ()
316+ // Provide dummy data for preview
317+ PhotoReasoningScreen (
318+ uiState = PhotoReasoningUiState .Success (" This is a sample response." ),
319+ onRequestScreenshotPermission = {},
320+ onReasonClicked = { _, _ -> }
321+ )
298322}
323+
324+ // Make sure you have this UriSaver or similar implementation
325+ // object UriSaver : Saver<MutableList<Uri>, List<String>> {
326+ // override fun SaverScope.save(value: MutableList<Uri>): List<String> {
327+ // return value.map { it.toString() }
328+ // }
329+ // override fun restore(value: List<String>): MutableList<Uri> {
330+ // return value.map { Uri.parse(it) }.toMutableList()
331+ // }
332+ // }
0 commit comments