@@ -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