diff --git a/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt b/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt index 31559163..91f6147e 100644 --- a/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt +++ b/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt @@ -6,9 +6,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.google.ai.client.generativeai.GenerativeModel import com.google.ai.client.generativeai.type.generationConfig -import com.google.ai.sample.feature.chat.ChatViewModel import com.google.ai.sample.feature.multimodal.PhotoReasoningViewModel -import com.google.ai.sample.feature.text.SummarizeViewModel // Model options enum class ModelOption(val displayName: String, val modelName: String) { @@ -72,16 +70,6 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory { return with(viewModelClass) { when { - isAssignableFrom(SummarizeViewModel::class.java) -> { - // Initialize a GenerativeModel with the currently selected model - // for text generation - val generativeModel = GenerativeModel( - modelName = currentModelName, - apiKey = apiKey, - generationConfig = config - ) - SummarizeViewModel(generativeModel) - } isAssignableFrom(PhotoReasoningViewModel::class.java) -> { // Initialize a GenerativeModel with the currently selected model @@ -96,15 +84,6 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory { PhotoReasoningViewModel(generativeModel, apiKeyManager) } - isAssignableFrom(ChatViewModel::class.java) -> { - // Initialize a GenerativeModel with the currently selected model for chat - val generativeModel = GenerativeModel( - modelName = currentModelName, - apiKey = apiKey, - generationConfig = config - ) - ChatViewModel(generativeModel) - } else -> throw IllegalArgumentException("Unknown ViewModel class: ${viewModelClass.name}") diff --git a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt index 3fa91a50..1b74a92c 100644 --- a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt +++ b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt @@ -24,9 +24,7 @@ import androidx.core.content.ContextCompat import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.google.ai.sample.feature.chat.ChatRoute import com.google.ai.sample.feature.multimodal.PhotoReasoningRoute -import com.google.ai.sample.feature.text.SummarizeRoute import com.google.ai.sample.ui.theme.GenerativeAISample class MainActivity : ComponentActivity() { @@ -127,15 +125,9 @@ class MainActivity : ComponentActivity() { } ) } - composable("summarize") { - SummarizeRoute() - } composable("photo_reasoning") { PhotoReasoningRoute() } - composable("chat") { - ChatRoute() - } } // Show API Key Dialog if needed diff --git a/app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt b/app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt index 13aaa343..56a6830e 100644 --- a/app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt @@ -54,9 +54,7 @@ fun MenuScreen( onApiKeyButtonClicked: () -> Unit = { } ) { val menuItems = listOf( - MenuItem("summarize", R.string.menu_summarize_title, R.string.menu_summarize_description), - MenuItem("photo_reasoning", R.string.menu_reason_title, R.string.menu_reason_description), - MenuItem("chat", R.string.menu_chat_title, R.string.menu_chat_description) + MenuItem("photo_reasoning", R.string.menu_reason_title, R.string.menu_reason_description) ) // Get current model diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt b/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt deleted file mode 100644 index ce76394b..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.sample.feature.chat - -import java.util.UUID - -enum class Participant { - USER, MODEL, ERROR -} - -data class ChatMessage( - val id: String = UUID.randomUUID().toString(), - var text: String = "", - val participant: Participant = Participant.USER, - var isPending: Boolean = false -) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt deleted file mode 100644 index d289294d..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt +++ /dev/null @@ -1,407 +0,0 @@ -package com.google.ai.sample.feature.chat - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Send -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.ai.sample.GenerativeViewModelFactory -import com.google.ai.sample.MainActivity -import com.google.ai.sample.R -import com.google.ai.sample.ScreenOperatorAccessibilityService -import com.google.ai.sample.util.Command - -@Composable -internal fun ChatRoute( - viewModel: ChatViewModel = viewModel(factory = GenerativeViewModelFactory) -) { - val chatUiState by viewModel.uiState.collectAsState() - val commandExecutionStatus by viewModel.commandExecutionStatus.collectAsState() - val detectedCommands by viewModel.detectedCommands.collectAsState() - - val context = LocalContext.current - val mainActivity = context as? MainActivity - - // Check if accessibility service is enabled - val isAccessibilityServiceEnabled = remember { - mutableStateOf( - mainActivity?.let { - ScreenOperatorAccessibilityService.isAccessibilityServiceEnabled(it) - } ?: false - ) - } - - // Check accessibility service status when the screen is composed - DisposableEffect(Unit) { - mainActivity?.checkAccessibilityServiceEnabled() - onDispose { } - } - - ChatScreen( - uiState = chatUiState, - commandExecutionStatus = commandExecutionStatus, - detectedCommands = detectedCommands, - onMessageSent = { messageText -> - viewModel.sendMessage(messageText) - }, - isAccessibilityServiceEnabled = isAccessibilityServiceEnabled.value, - onEnableAccessibilityService = { - mainActivity?.checkAccessibilityServiceEnabled() - } - ) -} - -@Composable -fun ChatScreen( - uiState: ChatUiState, - commandExecutionStatus: String = "", - detectedCommands: List = emptyList(), - onMessageSent: (String) -> Unit, - isAccessibilityServiceEnabled: Boolean = false, - onEnableAccessibilityService: () -> Unit = {} -) { - var userMessage by rememberSaveable { mutableStateOf("") } - val listState = rememberLazyListState() - - // Scroll to the bottom when new messages arrive - LaunchedEffect(uiState.messages.size) { - if (uiState.messages.isNotEmpty()) { - listState.animateScrollToItem(uiState.messages.size - 1) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(all = 16.dp) - ) { - // Accessibility Service Status Card - if (!isAccessibilityServiceEnabled) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Accessibility Service ist nicht aktiviert", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Die Klick-Funktionalität benötigt den Accessibility Service. Bitte aktivieren Sie ihn in den Einstellungen.", - color = MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.height(8.dp)) - TextButton( - onClick = onEnableAccessibilityService - ) { - Text("Accessibility Service aktivieren") - } - } - } - } - - // Command Execution Status - if (commandExecutionStatus.isNotEmpty()) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Befehlsstatus:", - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = commandExecutionStatus, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - - // Detected Commands - if (detectedCommands.isNotEmpty()) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Erkannte Befehle:", - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - - detectedCommands.forEachIndexed { index, command -> - val commandText = when (command) { - is Command.ClickButton -> "Klick auf Button: \"${command.buttonText}\"" - is Command.TapCoordinates -> "Tippen auf Koordinaten: (${command.x}, ${command.y})" - is Command.TakeScreenshot -> "Screenshot aufnehmen" - is Command.PressHomeButton -> "Home-Button drücken" - is Command.PressBackButton -> "Zurück-Button drücken" - is Command.ShowRecentApps -> "Übersicht der letzten Apps öffnen" - is Command.ScrollDown -> "Nach unten scrollen" - is Command.ScrollUp -> "Nach oben scrollen" - is Command.ScrollLeft -> "Nach links scrollen" - is Command.ScrollRight -> "Nach rechts scrollen" - is Command.ScrollDownFromCoordinates -> "Nach unten scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollUpFromCoordinates -> "Nach oben scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollLeftFromCoordinates -> "Nach links scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollRightFromCoordinates -> "Nach rechts scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.OpenApp -> "App öffnen: \"${command.packageName}\"" - is Command.WriteText -> "Text schreiben: \"${command.text}\"" - is Command.UseHighReasoningModel -> "Wechsle zu leistungsfähigerem Modell (gemini-2.5-pro-preview-03-25)" - is Command.UseLowReasoningModel -> "Wechsle zu schnellerem Modell (gemini-2.0-flash-lite)" - is Command.PressEnterKey -> "Enter command detected" - } - - Text( - text = "${index + 1}. $commandText", - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - - if (index < detectedCommands.size - 1) { - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.2f) - ) - } - } - } - } - } - - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - items(uiState.messages) { message -> - when (message.participant) { - Participant.USER -> { - UserChatBubble( - text = message.text, - isPending = message.isPending - ) - } - Participant.MODEL -> { - ModelChatBubble( - text = message.text, - isPending = message.isPending - ) - } - Participant.ERROR -> { - ErrorChatBubble( - text = message.text - ) - } - } - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - value = userMessage, - onValueChange = { userMessage = it }, - placeholder = { Text(stringResource(R.string.chat_label)) }, - modifier = Modifier - .weight(1f) - .padding(end = 8.dp) - ) - IconButton( - onClick = { - if (userMessage.isNotBlank()) { - onMessageSent(userMessage) - userMessage = "" - } - } - ) { - Icon( - Icons.Default.Send, - contentDescription = stringResource(R.string.action_send), - tint = MaterialTheme.colorScheme.primary - ) - } - } - } -} - -@Composable -fun UserChatBubble( - text: String, - isPending: Boolean -) { - Row( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 8.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.Top - ) { - Spacer(modifier = Modifier.weight(1f)) - Card( - shape = MaterialTheme.shapes.medium, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - modifier = Modifier.weight(4f) - ) { - Column( - modifier = Modifier.padding(all = 16.dp) - ) { - Text( - text = text, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - if (isPending) { - CircularProgressIndicator( - modifier = Modifier - .padding(top = 8.dp) - .requiredSize(16.dp), - strokeWidth = 2.dp - ) - } - } - } - } -} - -@Composable -fun ModelChatBubble( - text: String, - isPending: Boolean -) { - Row( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 8.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.Top - ) { - Card( - shape = MaterialTheme.shapes.medium, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ), - modifier = Modifier.weight(4f) - ) { - Row( - modifier = Modifier.padding(all = 16.dp) - ) { - Icon( - Icons.Outlined.Person, - contentDescription = "AI Assistant", - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier - .requiredSize(24.dp) - .drawBehind { - drawCircle(color = Color.White) - } - .padding(end = 8.dp) - ) - Column { - Text( - text = text, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - if (isPending) { - CircularProgressIndicator( - modifier = Modifier - .padding(top = 8.dp) - .requiredSize(16.dp), - strokeWidth = 2.dp - ) - } - } - } - } - Spacer(modifier = Modifier.weight(1f)) - } -} - -@Composable -fun ErrorChatBubble( - text: String -) { - Box( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 8.dp) - .fillMaxWidth() - ) { - Card( - shape = MaterialTheme.shapes.medium, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = text, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(all = 16.dp) - ) - } - } -} diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt b/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt deleted file mode 100644 index 8d8985e5..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.sample.feature.chat - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.tooling.preview.Preview -import com.google.ai.sample.MenuScreen - -class ChatUiState( - messages: List = emptyList() -) { - private val _messages: MutableList = messages.toMutableStateList() - val messages: List = _messages - - fun addMessage(msg: ChatMessage) { - _messages.add(msg) - } - - fun replaceLastPendingMessage() { - val lastMessage = _messages.lastOrNull() - lastMessage?.let { - val newMessage = lastMessage.apply { isPending = false } - _messages.removeLast() - _messages.add(newMessage) - } - } -} diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt deleted file mode 100644 index ec3946ea..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt +++ /dev/null @@ -1,241 +0,0 @@ -package com.google.ai.sample.feature.chat - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.ai.client.generativeai.GenerativeModel -import com.google.ai.client.generativeai.type.asTextOrNull -import com.google.ai.client.generativeai.type.content -import com.google.ai.sample.MainActivity -import com.google.ai.sample.ScreenOperatorAccessibilityService -import com.google.ai.sample.util.Command -import com.google.ai.sample.util.CommandParser -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import android.util.Log - -class ChatViewModel( - generativeModel: GenerativeModel -) : ViewModel() { - private val TAG = "ChatViewModel" - - private val chat = generativeModel.startChat( - history = listOf( - content(role = "user") { text("Hello, I have 2 dogs in my house.") }, - content(role = "model") { text("Great to meet you. What would you like to know?") } - ) - ) - - private val _uiState: MutableStateFlow = - MutableStateFlow(ChatUiState(chat.history.map { content -> - // Map the initial messages - ChatMessage( - text = content.parts.first().asTextOrNull() ?: "", - participant = if (content.role == "user") Participant.USER else Participant.MODEL, - isPending = false - ) - })) - val uiState: StateFlow = - _uiState.asStateFlow() - - // Keep track of detected commands - private val _detectedCommands = MutableStateFlow>(emptyList()) - val detectedCommands: StateFlow> = _detectedCommands.asStateFlow() - - // Keep track of command execution status - private val _commandExecutionStatus = MutableStateFlow("") - val commandExecutionStatus: StateFlow = _commandExecutionStatus.asStateFlow() - - fun sendMessage(userMessage: String) { - // Add a pending message - _uiState.value.addMessage( - ChatMessage( - text = userMessage, - participant = Participant.USER, - isPending = true - ) - ) - - // Clear previous commands - _detectedCommands.value = emptyList() - _commandExecutionStatus.value = "" - - viewModelScope.launch { - try { - val response = chat.sendMessage(userMessage) - - _uiState.value.replaceLastPendingMessage() - - response.text?.let { modelResponse -> - _uiState.value.addMessage( - ChatMessage( - text = modelResponse, - participant = Participant.MODEL, - isPending = false - ) - ) - - // Process commands in the response - processCommands(modelResponse) - } - } catch (e: Exception) { - _uiState.value.replaceLastPendingMessage() - _uiState.value.addMessage( - ChatMessage( - text = e.localizedMessage, - participant = Participant.ERROR - ) - ) - - // Update command execution status - _commandExecutionStatus.value = "Fehler: ${e.localizedMessage}" - } - } - } - - /** - * Process commands found in the AI response - */ - private fun processCommands(text: String) { - viewModelScope.launch(Dispatchers.Main) { - try { - // Parse commands from the text - val commands = CommandParser.parseCommands(text) - - if (commands.isNotEmpty()) { - Log.d(TAG, "Found ${commands.size} commands in response") - - // Update the detected commands - val currentCommands = _detectedCommands.value.toMutableList() - currentCommands.addAll(commands) - _detectedCommands.value = currentCommands - - // Update status to show commands were detected - val commandDescriptions = commands.map { - when (it) { - is Command.ClickButton -> "Klick auf Button: \"${it.buttonText}\"" - is Command.TapCoordinates -> "Tippen auf Koordinaten: (${it.x}, ${it.y})" - is Command.TakeScreenshot -> "Screenshot aufnehmen" - is Command.PressHomeButton -> "Home-Button drücken" - is Command.PressBackButton -> "Zurück-Button drücken" - is Command.ShowRecentApps -> "Übersicht der letzten Apps öffnen" - is Command.ScrollDown -> "Nach unten scrollen" - is Command.ScrollUp -> "Nach oben scrollen" - is Command.ScrollLeft -> "Nach links scrollen" - is Command.ScrollRight -> "Nach rechts scrollen" - is Command.ScrollDownFromCoordinates -> "Nach unten scrollen von Position (${it.x}, ${it.y}) mit Distanz ${it.distance}px und Dauer ${it.duration}ms" - is Command.ScrollUpFromCoordinates -> "Nach oben scrollen von Position (${it.x}, ${it.y}) mit Distanz ${it.distance}px und Dauer ${it.duration}ms" - is Command.ScrollLeftFromCoordinates -> "Nach links scrollen von Position (${it.x}, ${it.y}) mit Distanz ${it.distance}px und Dauer ${it.distance}ms" - is Command.ScrollRightFromCoordinates -> "Nach rechts scrollen von Position (${it.x}, ${it.y}) mit Distanz ${it.distance}px und Dauer ${it.duration}ms" - is Command.OpenApp -> "App öffnen: \"${it.packageName}\"" - is Command.WriteText -> "Text schreiben: \"${it.text}\"" - is Command.UseHighReasoningModel -> "Wechsle zu leistungsfähigerem Modell (gemini-2.5-pro-preview-03-25)" - is Command.UseLowReasoningModel -> "Wechsle zu schnellerem Modell (gemini-2.0-flash-lite)" - is Command.PressEnterKey -> "Enter command detected" - } - } - - // Show toast with detected commands - val mainActivity = MainActivity.getInstance() - mainActivity?.updateStatusMessage( - "Befehle erkannt: ${commandDescriptions.joinToString(", ")}", - false - ) - - // Update status - _commandExecutionStatus.value = "Befehle erkannt: ${commandDescriptions.joinToString(", ")}" - - // Check if accessibility service is enabled - val isServiceEnabled = mainActivity?.let { - ScreenOperatorAccessibilityService.isAccessibilityServiceEnabled(it) - } ?: false - - if (!isServiceEnabled) { - Log.e(TAG, "Accessibility service is not enabled") - _commandExecutionStatus.value = "Accessibility Service ist nicht aktiviert. Bitte aktivieren Sie den Service in den Einstellungen." - - // Prompt user to enable accessibility service - mainActivity?.checkAccessibilityServiceEnabled() - return@launch - } - - // Check if service is available - if (!ScreenOperatorAccessibilityService.isServiceAvailable()) { - Log.e(TAG, "Accessibility service is not available") - _commandExecutionStatus.value = "Accessibility Service ist nicht verfügbar. Bitte starten Sie die App neu." - - // Show toast - mainActivity?.updateStatusMessage( - "Accessibility Service ist nicht verfügbar. Bitte starten Sie die App neu.", - true - ) - return@launch - } - - // Execute each command - commands.forEachIndexed { index, command -> - Log.d(TAG, "Executing command: $command") - - // Update status to show command is being executed - val commandDescription = when (command) { - is Command.ClickButton -> "Klick auf Button: \"${command.buttonText}\"" - is Command.TapCoordinates -> "Tippen auf Koordinaten: (${command.x}, ${command.y})" - is Command.TakeScreenshot -> "Screenshot aufnehmen" - is Command.PressHomeButton -> "Home-Button drücken" - is Command.PressBackButton -> "Zurück-Button drücken" - is Command.ShowRecentApps -> "Übersicht der letzten Apps öffnen" - is Command.ScrollDown -> "Nach unten scrollen" - is Command.ScrollUp -> "Nach oben scrollen" - is Command.ScrollLeft -> "Nach links scrollen" - is Command.ScrollRight -> "Nach rechts scrollen" - is Command.ScrollDownFromCoordinates -> "Nach unten scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollUpFromCoordinates -> "Nach oben scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollLeftFromCoordinates -> "Nach links scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.ScrollRightFromCoordinates -> "Nach rechts scrollen von Position (${command.x}, ${command.y}) mit Distanz ${command.distance}px und Dauer ${command.duration}ms" - is Command.OpenApp -> "App öffnen: \"${command.packageName}\"" - is Command.WriteText -> "Text schreiben: \"${command.text}\"" - is Command.UseHighReasoningModel -> "Wechsle zu leistungsfähigerem Modell (gemini-2.5-pro-preview-03-25)" - is Command.UseLowReasoningModel -> "Wechsle zu schnellerem Modell (gemini-2.0-flash-lite)" - is Command.PressEnterKey -> "Enter Command simulated" - } - - _commandExecutionStatus.value = "Führe aus: $commandDescription (${index + 1}/${commands.size})" - - // Show toast with command being executed - mainActivity?.updateStatusMessage( - "Führe aus: $commandDescription", - false - ) - - // Execute the command - ScreenOperatorAccessibilityService.executeCommand(command) - - // Add a small delay between commands to avoid overwhelming the system - kotlinx.coroutines.delay(800) - } - - // Update status to show all commands were executed - _commandExecutionStatus.value = "Alle Befehle ausgeführt: ${commandDescriptions.joinToString(", ")}" - - // Show toast with all commands executed - mainActivity?.updateStatusMessage( - "Alle Befehle ausgeführt", - false - ) - } - } catch (e: Exception) { - Log.e(TAG, "Error processing commands: ${e.message}", e) - _commandExecutionStatus.value = "Fehler bei der Befehlsverarbeitung: ${e.message}" - - // Show toast with error - val mainActivity = MainActivity.getInstance() - mainActivity?.updateStatusMessage( - "Fehler bei der Befehlsverarbeitung: ${e.message}", - true - ) - } - } - } -} diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeScreen.kt deleted file mode 100644 index dc1ef9ae..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeScreen.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.sample.feature.text - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.ai.sample.GenerativeViewModelFactory -import com.google.ai.sample.R -import com.google.ai.sample.ui.theme.GenerativeAISample - -@Composable -internal fun SummarizeRoute( - summarizeViewModel: SummarizeViewModel = viewModel(factory = GenerativeViewModelFactory) -) { - val summarizeUiState by summarizeViewModel.uiState.collectAsState() - - SummarizeScreen(summarizeUiState, onSummarizeClicked = { inputText -> - summarizeViewModel.summarizeStreaming(inputText) - }) -} - -@Composable -fun SummarizeScreen( - uiState: SummarizeUiState = SummarizeUiState.Loading, - onSummarizeClicked: (String) -> Unit = {} -) { - var textToSummarize by rememberSaveable { mutableStateOf("") } - - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - ) { - ElevatedCard( - modifier = Modifier - .padding(all = 16.dp) - .fillMaxWidth(), - shape = MaterialTheme.shapes.large - ) { - OutlinedTextField( - value = textToSummarize, - label = { Text(stringResource(R.string.summarize_label)) }, - placeholder = { Text(stringResource(R.string.summarize_hint)) }, - onValueChange = { textToSummarize = it }, - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) - TextButton( - onClick = { - if (textToSummarize.isNotBlank()) { - onSummarizeClicked(textToSummarize) - } - }, - modifier = Modifier - .padding(end = 16.dp, bottom = 16.dp) - .align(Alignment.End) - ) { - Text(stringResource(R.string.action_go)) - } - } - - when (uiState) { - SummarizeUiState.Initial -> { - // Nothing is shown - } - - SummarizeUiState.Loading -> { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(all = 8.dp) - .align(Alignment.CenterHorizontally) - ) { - CircularProgressIndicator() - } - } - - is SummarizeUiState.Success -> { - Card( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - shape = MaterialTheme.shapes.large, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) { - Row( - modifier = Modifier - .padding(all = 16.dp) - .fillMaxWidth() - ) { - Icon( - Icons.Outlined.Person, - contentDescription = "Person Icon", - tint = MaterialTheme.colorScheme.onSecondary, - modifier = Modifier - .requiredSize(36.dp) - .drawBehind { - drawCircle(color = Color.White) - } - ) - Text( - text = uiState.outputText, // TODO(thatfiredev): Figure out Markdown support - color = MaterialTheme.colorScheme.onSecondary, - modifier = Modifier - .padding(start = 16.dp) - .fillMaxWidth() - ) - } - } - } - - is SummarizeUiState.Error -> { - Card( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - shape = MaterialTheme.shapes.large, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Text( - text = uiState.errorMessage, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(all = 16.dp) - ) - } - } - } - } -} - -@Composable -@Preview(showSystemUi = true) -fun SummarizeScreenPreview() { - GenerativeAISample(darkTheme = true) { - SummarizeScreen() - } -} diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeUiState.kt b/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeUiState.kt deleted file mode 100644 index b0598131..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeUiState.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.sample.feature.text - -/** - * A sealed hierarchy describing the state of the text generation. - */ -sealed interface SummarizeUiState { - - /** - * Empty state when the screen is first shown - */ - data object Initial: SummarizeUiState - - /** - * Still loading - */ - data object Loading: SummarizeUiState - - /** - * Text has been generated - */ - data class Success( - val outputText: String - ): SummarizeUiState - - /** - * There was an error generating text - */ - data class Error( - val errorMessage: String - ): SummarizeUiState -} diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeViewModel.kt deleted file mode 100644 index f0c20994..00000000 --- a/app/src/main/kotlin/com/google/ai/sample/feature/text/SummarizeViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.sample.feature.text - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.ai.client.generativeai.GenerativeModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -class SummarizeViewModel( - private val generativeModel: GenerativeModel -) : ViewModel() { - - private val _uiState: MutableStateFlow = - MutableStateFlow(SummarizeUiState.Initial) - val uiState: StateFlow = - _uiState.asStateFlow() - - fun summarize(inputText: String) { - _uiState.value = SummarizeUiState.Loading - - val prompt = "Summarize the following text for me: $inputText" - - viewModelScope.launch { - // Non-streaming - try { - val response = generativeModel.generateContent(prompt) - response.text?.let { outputContent -> - _uiState.value = SummarizeUiState.Success(outputContent) - } - } catch (e: Exception) { - _uiState.value = SummarizeUiState.Error(e.localizedMessage ?: "") - } - } - } - - fun summarizeStreaming(inputText: String) { - _uiState.value = SummarizeUiState.Loading - - val prompt = "Summarize the following text for me: $inputText" - - viewModelScope.launch { - try { - var outputContent = "" - generativeModel.generateContentStream(prompt) - .collect { response -> - outputContent += response.text - _uiState.value = SummarizeUiState.Success(outputContent) - } - } catch (e: Exception) { - _uiState.value = SummarizeUiState.Error(e.localizedMessage ?: "") - } - } - } -}