Skip to content

Commit e2598a2

Browse files
Refine stop operation handling and UI reset
This commit addresses several issues related to the stop functionality: 1. **Consolidated Stop Messages:** I modified `sendMessageWithRetry` to prevent it from generating "Operation cancelled" messages when a user-initiated stop (via `onStopClicked`) is the cause. `onStopClicked` is now the primary source for the "Operation stopped by user" chat message. 2. **Prevented Cancellation Message During Error on Stop:** The `catch` block in `sendMessageWithRetry` no longer produces an "Operation cancelled during error handling" message or UI update if the `stopExecutionFlag` was already true. Such exceptions are now only logged, allowing the user-initiated stop state to prevail. 3. **Stop Button UI Reset:** `onStopClicked` now sets the UI state to `PhotoReasoningUiState.Success("Operation stopped.")` after all stop operations are complete. This allows the UI (including the stop button) to return to an interactive state, ready for new input, addressing an issue where the button would remain in a disabled state.
1 parent a744a76 commit e2598a2

1 file changed

Lines changed: 79 additions & 24 deletions

File tree

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

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,15 @@ class PhotoReasoningViewModel(
207207
_chatMessagesFlow.value = chatMessages
208208

209209

210-
_uiState.value = PhotoReasoningUiState.Stopped
210+
// _uiState.value = PhotoReasoningUiState.Stopped; // No longer setting this as the final state.
211211
_commandExecutionStatus.value = "Stopped."
212212
_detectedCommands.value = emptyList()
213-
Log.d(TAG, "Stop clicked, operations cancelled, UI updated to Stopped state.")
213+
Log.d(TAG, "Stop clicked, operations cancelled.")
214+
215+
// Set a success state to indicate the stop operation itself was successful
216+
// and the UI can return to an idle/interactive state.
217+
_uiState.value = PhotoReasoningUiState.Success("Operation stopped.")
218+
Log.d(TAG, "UI updated to Success state after stop.")
214219
}
215220

216221
/**
@@ -220,20 +225,32 @@ class PhotoReasoningViewModel(
220225
* @param retryCount The current retry count
221226
*/
222227
private suspend fun sendMessageWithRetry(inputContent: Content, retryCount: Int) {
223-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Check for cancellation
224-
_uiState.value = PhotoReasoningUiState.Success("Operation cancelled before sending.")
225-
updateAiMessage("Operation cancelled.")
226-
return
228+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
229+
if (stopExecutionFlag.get()) {
230+
// User initiated stop, onStopClicked will handle UI and message
231+
return
232+
} else {
233+
// Cancellation not by user stop button
234+
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly before sending.")
235+
updateAiMessage("Operation cancelled unexpectedly.")
236+
return
237+
}
227238
}
228239
var shouldProceed = true // Flag to control further processing
229240

230241
try {
231242
// Send the message to the chat to maintain context
232243
val response = chat.sendMessage(inputContent)
233-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Check for cancellation
234-
_uiState.value = PhotoReasoningUiState.Success("Operation cancelled after sending.")
235-
updateAiMessage("Operation cancelled.")
236-
return
244+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
245+
if (stopExecutionFlag.get()) {
246+
// User initiated stop, onStopClicked will handle UI and message
247+
return
248+
} else {
249+
// Cancellation not by user stop button
250+
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly after sending.")
251+
updateAiMessage("Operation cancelled unexpectedly.")
252+
return
253+
}
237254
}
238255

239256
var outputContent = ""
@@ -242,19 +259,32 @@ class PhotoReasoningViewModel(
242259
response.text?.let { modelResponse ->
243260
outputContent = modelResponse
244261

245-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Check for cancellation
246-
_uiState.value = PhotoReasoningUiState.Success("Operation cancelled during response processing.")
247-
updateAiMessage("Operation cancelled.")
248-
shouldProceed = false // Signal to skip further processing
262+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
263+
if (stopExecutionFlag.get()) {
264+
// User initiated stop, onStopClicked will handle UI and message
265+
shouldProceed = false // Signal to skip further processing
266+
} else {
267+
// Cancellation not by user stop button
268+
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly during response processing.")
269+
updateAiMessage("Operation cancelled unexpectedly.")
270+
shouldProceed = false // Signal to skip further processing
271+
}
249272
}
250273
}
251274

252275
if (shouldProceed) { // Only proceed if not cancelled in the 'let' block
253276
withContext(Dispatchers.Main) {
254277
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Re-check for cancellation
255-
_uiState.value = PhotoReasoningUiState.Success("Operation cancelled.")
256-
updateAiMessage("Operation cancelled.")
278+
if (stopExecutionFlag.get()) {
279+
// User initiated stop, onStopClicked will handle UI and message
280+
// No action needed here, shouldProceed will prevent further execution if already false
281+
// or the outer checks in this function will catch it.
282+
} else {
283+
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly before UI update.")
284+
updateAiMessage("Operation cancelled unexpectedly.")
285+
}
257286
// No return@withContext, logic will naturally skip due to outer 'if (shouldProceed)' and this check
287+
// or if shouldProceed was set to false earlier.
258288
} else {
259289
_uiState.value = PhotoReasoningUiState.Success(outputContent)
260290

@@ -283,32 +313,49 @@ class PhotoReasoningViewModel(
283313
}
284314
}
285315
} catch (e: Exception) {
286-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Check for cancellation during exception handling
287-
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled during error handling.")
288-
updateAiMessage("Operation cancelled during error handling.")
316+
if (stopExecutionFlag.get()) {
317+
// If user already stopped, just log the error and return.
318+
// Do not update UI or send chat messages as onStopClicked handles this.
319+
Log.w(TAG, "Exception caught after stop flag was set: ${e.message}", e)
320+
return
321+
}
322+
// If the stop flag is not set, but the job is inactive (cancelled by other means)
323+
if (currentReasoningJob?.isActive != true) {
324+
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled and then an error occurred.") // Or a more fitting message
325+
updateAiMessage("Operation cancelled, error during cleanup: ${e.message}")
326+
Log.e(TAG, "Error generating content after job was cancelled: ${e.message}", e)
289327
return
290328
}
329+
330+
// If stopExecutionFlag is false AND currentReasoningJob is active, proceed with normal error handling.
291331
Log.e(TAG, "Error generating content: ${e.message}", e)
292332

293333
// Check specifically for quota exceeded errors first
294334
if (isQuotaExceededError(e) && apiKeyManager != null) {
295-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return // Check for cancellation
335+
// The check currentReasoningJob?.isActive != true is still relevant if the job gets cancelled
336+
// by other means after the top checks in this catch block.
337+
// stopExecutionFlag.get() is less likely to be true here due to the top check, but kept for defense.
338+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return
296339
handleQuotaExceededError(e, inputContent, retryCount)
297340
return
298341
}
299342

300343
// Check for other 503 errors
301344
if (is503Error(e) && apiKeyManager != null) {
302-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return // Check for cancellation
345+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return
303346
handle503Error(e, inputContent, retryCount)
304347
return
305348
}
306349

307350
// If we get here, it's not a 503 error or quota exceeded error
351+
// The stopExecutionFlag.get() check here is mostly redundant due to the top check,
352+
// but currentReasoningJob?.isActive is still a valid check.
308353
if (currentReasoningJob?.isActive == true && !stopExecutionFlag.get()) {
309354
withContext(Dispatchers.Main) {
310355
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
311-
// If cancelled, potentially update UI or log, but don't proceed with error state for this exception
356+
// If cancelled (possibly between the outer check and here, or due to job inactivity)
357+
// Potentially log or update UI to reflect this specific state if desired.
358+
// For now, this means the error e won't be set as the primary UI state.
312359
} else {
313360
_uiState.value = PhotoReasoningUiState.Error(e.localizedMessage ?: "Unknown error")
314361
_commandExecutionStatus.value = "Error during generation: ${e.localizedMessage}"
@@ -328,8 +375,16 @@ class PhotoReasoningViewModel(
328375
}
329376
}
330377
} else {
331-
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled during error processing.")
332-
updateAiMessage("Operation cancelled during error processing.")
378+
// This branch could be reached if:
379+
// 1. stopExecutionFlag was set by another thread between the top check and here.
380+
// 2. currentReasoningJob became inactive for reasons other than the stop flag.
381+
// If stopExecutionFlag is true, onStopClicked handles the message.
382+
// If only job inactive, a general "cancelled during error processing" might be okay,
383+
// but the specific check for job inactivity at the top of the catch block should handle most cases.
384+
if (!stopExecutionFlag.get()) {
385+
_uiState.value = PhotoReasoningUiState.Error("Operation error processing or cancelled.")
386+
updateAiMessage("Operation error processing or cancelled.")
387+
}
333388
}
334389
}
335390
}

0 commit comments

Comments
 (0)