@@ -114,6 +114,12 @@ class PhotoReasoningViewModel(
114114
115115 private val _showStopNotificationFlow = MutableStateFlow (false )
116116 val showStopNotificationFlow: StateFlow <Boolean > = _showStopNotificationFlow .asStateFlow()
117+
118+ private val _isGenerationRunningFlow = MutableStateFlow (false )
119+ val isGenerationRunningFlow: StateFlow <Boolean > = _isGenerationRunningFlow .asStateFlow()
120+
121+ private val _isOfflineGpuModelLoadedFlow = MutableStateFlow (false )
122+ val isOfflineGpuModelLoadedFlow: StateFlow <Boolean > = _isOfflineGpuModelLoadedFlow .asStateFlow()
117123
118124 // Keep track of the latest screenshot URI
119125 private var latestScreenshotUri: Uri ? = null
@@ -389,11 +395,32 @@ class PhotoReasoningViewModel(
389395 }
390396 }
391397
398+ private fun isGenerationRunning (): Boolean {
399+ val lastMessage = _chatState .getAllMessages().lastOrNull()
400+ val hasPendingModelMessage =
401+ lastMessage?.participant == PhotoParticipant .MODEL && lastMessage.isPending
402+ val hasActiveJob =
403+ currentReasoningJob?.isActive == true || commandProcessingJob?.isActive == true
404+ return hasPendingModelMessage || hasActiveJob
405+ }
406+
407+ private fun isOfflineGpuModelLoaded (): Boolean {
408+ return com.google.ai.sample.GenerativeAiViewModelFactory .getCurrentModel() == ModelOption .GEMMA_3N_E4B_IT &&
409+ com.google.ai.sample.GenerativeAiViewModelFactory .getBackend() == InferenceBackend .GPU &&
410+ llmInference != null
411+ }
412+
413+ private fun refreshStopButtonState () {
414+ _isGenerationRunningFlow .value = isGenerationRunning()
415+ _isOfflineGpuModelLoadedFlow .value = isOfflineGpuModelLoaded()
416+ }
417+
392418 fun closeOfflineModel () {
393419 try {
394420 llmInference?.close()
395421 llmInference = null
396422 System .gc()
423+ refreshStopButtonState()
397424 Log .d(TAG , " Offline model explicitly closed to free RAM" )
398425 } catch (e: Exception ) {
399426 Log .w(TAG , " Error closing offline model" , e)
@@ -1073,18 +1100,27 @@ class PhotoReasoningViewModel(
10731100 }
10741101
10751102 fun onStopClicked () {
1076- if (isLiveMode) {
1077- // For live mode, close the connection
1078- liveApiManager?.close()
1103+ _showStopNotificationFlow .value = false
1104+
1105+ val generationRunning = isGenerationRunning()
1106+
1107+ // Kein aktiver Lauf: zweiter Klick => Modell entladen, keine Chat-Nachricht
1108+ if (! generationRunning) {
1109+ if (isOfflineGpuModelLoaded()) {
1110+ closeOfflineModel()
1111+ Log .d(TAG , " Stop clicked while idle: offline GPU model closed to free RAM" )
1112+ } else {
1113+ refreshStopButtonState()
1114+ Log .d(TAG , " Stop clicked while idle: nothing to stop and no offline GPU model loaded" )
1115+ }
1116+ return
10791117 }
10801118
1081- // Close offline model instance to force stop generation or just release RAM
1082- if (com.google.ai.sample. GenerativeAiViewModelFactory .getCurrentModel() == ModelOption . GEMMA_3N_E4B_IT ) {
1083- closeOfflineModel ()
1119+ // Aktive Generierung: nur stoppen, Modell NICHT direkt schließen
1120+ if (isLiveMode ) {
1121+ liveApiManager?.close ()
10841122 }
10851123
1086- // Rest of the existing onStopClicked code
1087- _showStopNotificationFlow .value = false
10881124 stopExecutionFlag.set(true )
10891125 currentReasoningJob?.cancel()
10901126 commandProcessingJob?.cancel()
@@ -1103,31 +1139,28 @@ class PhotoReasoningViewModel(
11031139 )
11041140 )
11051141 } else if (lastMessage != null && lastMessage.participant == PhotoParticipant .MODEL && ! lastMessage.isPending) {
1106- // If the last message was a successful model response, update it.
1107- messages[messages.size - 1 ] = lastMessage.copy(text = lastMessage.text + " \n\n [Stopped by user]" )
1142+ messages[messages.lastIndex] =
1143+ lastMessage.copy(text = lastMessage.text + " \n\n [Stopped by user]" )
11081144 } else {
1109- // If no relevant model message, or last message was user/error, add a new model message
1110- messages.add(
1145+ messages.add(
11111146 PhotoReasoningMessage (
11121147 text = statusMessage,
11131148 participant = PhotoParticipant .MODEL ,
11141149 isPending = false
11151150 )
11161151 )
11171152 }
1153+
11181154 _chatState .setAllMessages(messages)
11191155 _chatMessagesFlow .value = _chatState .getAllMessages()
1120-
1121-
1122- // _uiState.value = PhotoReasoningUiState.Stopped; // No longer setting this as the final state.
1123- _commandExecutionStatus .value = " " // Set to empty string
1156+ _commandExecutionStatus .value = " "
11241157 _detectedCommands .value = emptyList()
11251158 Log .d(TAG , " Stop clicked, operations cancelled, command status cleared." )
11261159
1127- // Set a success state to indicate the stop operation itself was successful
1128- // and the UI can return to an idle/interactive state.
11291160 _uiState .value = PhotoReasoningUiState .Success (" Operation stopped." )
11301161 Log .d(TAG , " UI updated to Success state after stop." )
1162+
1163+ refreshStopButtonState()
11311164 }
11321165
11331166 /* *
@@ -1328,42 +1361,40 @@ class PhotoReasoningViewModel(
13281361 viewModelScope.launch(Dispatchers .Main ) {
13291362 replaceAiMessageText(" Expert found! Requesting screen capture permission..." , isPending = true )
13301363
1331- // Request a fresh MediaProjection specifically for WebRTC
1332- // This does NOT start ScreenCaptureService - avoids token reuse crash
1364+ // Request a fresh MediaProjection specifically for WebRTC.
1365+ // MainActivity startet bereits ACTION_KEEP_ALIVE_FOR_WEBRTC BEVOR dieser Callback gerufen wird.
1366+ // Kein weiterer startForegroundService()-Aufruf nötig - verhindert ForegroundServiceDidNotStartInTimeException.
13331367 val mainActivity = MainActivity .getInstance()
13341368 if (mainActivity != null ) {
13351369 mainActivity.requestMediaProjectionForWebRTC { resultCode, resultData ->
1336- Log .d(TAG , " WebRTC MediaProjection granted. Starting foreground service first, then screen capture ." )
1370+ Log .d(TAG , " WebRTC MediaProjection granted. Service läuft bereits via KEEP_ALIVE. Starte Screen Capture ." )
13371371 replaceAiMessageText(" Establishing video connection..." , isPending = true )
13381372
1339- // Task 1: Only start ScreenCaptureService if not already running
1340- // This prevents ForegroundServiceDidNotStartInTimeException
1341- if (! ScreenCaptureService .isRunning()) {
1342- val serviceIntent = Intent (mainActivity, ScreenCaptureService ::class .java).apply {
1343- action = ScreenCaptureService .ACTION_START_CAPTURE
1344- putExtra(ScreenCaptureService .EXTRA_RESULT_CODE , resultCode)
1345- putExtra(ScreenCaptureService .EXTRA_RESULT_DATA , resultData)
1346- putExtra(ScreenCaptureService .EXTRA_TAKE_SCREENSHOT_ON_START , false )
1347- }
1348- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
1349- mainActivity.startForegroundService(serviceIntent)
1350- } else {
1351- mainActivity.startService(serviceIntent)
1352- }
1353- } else {
1354- Log .d(TAG , " ScreenCaptureService already running, skipping service start." )
1355- }
1373+ // KEIN startForegroundService() hier - MainActivity hat bereits ACTION_KEEP_ALIVE_FOR_WEBRTC gesendet.
1374+ // Das vermeidet doppelten Service-Start und ForegroundServiceDidNotStartInTimeException.
13561375
1357- // Delay to ensure foreground service is up before WebRTC capture
13581376 viewModelScope.launch {
1359- delay(500 )
1360- // Start screen capture for WebRTC with fresh permission data
1361- webRTCSender?.startScreenCapture(resultData)
1362- webRTCSender?.createPeerConnection()
1363-
1364- // Create Offer
1365- webRTCSender?.createOffer { sdp ->
1366- signalingClient?.sendOffer(sdp)
1377+ // Kurze Verzögerung zur Stabilisierung des Foreground-Services
1378+ delay(300 )
1379+ try {
1380+ // Start screen capture for WebRTC with fresh permission data
1381+ webRTCSender?.startScreenCapture(resultData)
1382+ webRTCSender?.createPeerConnection()
1383+
1384+ // Create Offer
1385+ webRTCSender?.createOffer { sdp ->
1386+ signalingClient?.sendOffer(sdp)
1387+ }
1388+ } catch (e: SecurityException ) {
1389+ Log .e(TAG , " SecurityException beim WebRTC Screen Capture - MediaProjection Token ungültig?" , e)
1390+ viewModelScope.launch(Dispatchers .Main ) {
1391+ _uiState .value = PhotoReasoningUiState .Error (" Screen capture permission expired. Please try again." )
1392+ }
1393+ } catch (e: Exception ) {
1394+ Log .e(TAG , " Fehler beim Starten des WebRTC Screen Capture" , e)
1395+ viewModelScope.launch(Dispatchers .Main ) {
1396+ _uiState .value = PhotoReasoningUiState .Error (" Video connection failed: ${e.message} " )
1397+ }
13671398 }
13681399 }
13691400 }
0 commit comments