├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── compiler.xml ├── AndroidProjectSystem.xml ├── migrations.xml ├── deploymentTargetSelector.xml └── runConfigurations.xml ├── app ├── src │ ├── main │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── aisecretary │ │ │ │ ├── data │ │ │ │ ├── model │ │ │ │ │ ├── ConversationContext.kt │ │ │ │ │ ├── Message.kt │ │ │ │ │ └── MemoryFact.kt │ │ │ │ ├── local │ │ │ │ │ ├── database │ │ │ │ │ │ ├── dao │ │ │ │ │ │ │ ├── MessageDao.kt │ │ │ │ │ │ │ └── MemoryFactDao.kt │ │ │ │ │ │ └── AppDatabase.kt │ │ │ │ │ └── preferences │ │ │ │ │ │ ├── UserPreferences.kt │ │ │ │ │ │ └── SettingsManager.kt │ │ │ │ └── repository │ │ │ │ │ ├── VoiceRepository.kt │ │ │ │ │ └── ChatRepository.kt │ │ │ │ ├── ai │ │ │ │ ├── rag │ │ │ │ │ ├── DocumentStore.kt │ │ │ │ │ ├── Retriever.kt │ │ │ │ │ └── VectorStore.kt │ │ │ │ ├── memory │ │ │ │ │ ├── ConversationMemory.kt │ │ │ │ │ ├── MemoryRetriever.kt │ │ │ │ │ └── MemoryManager.kt │ │ │ │ ├── llm │ │ │ │ │ ├── OllamaService.kt │ │ │ │ │ ├── model │ │ │ │ │ │ ├── OllamaResponse.kt │ │ │ │ │ │ └── OllamaRequest.kt │ │ │ │ │ └── LlamaClient.kt │ │ │ │ └── voice │ │ │ │ │ ├── TextToSpeech.kt │ │ │ │ │ └── SpeechRecognizer.kt │ │ │ │ ├── di │ │ │ │ └── AppModule.kt │ │ │ │ ├── ui │ │ │ │ ├── chat │ │ │ │ │ ├── MessageAdapter.kt │ │ │ │ │ ├── ChatViewModel.kt │ │ │ │ │ └── ChatFragment.kt │ │ │ │ ├── memory │ │ │ │ │ ├── MemoryAdapter.kt │ │ │ │ │ ├── MemoryViewModel.kt │ │ │ │ │ └── MemoryFragment.kt │ │ │ │ └── settings │ │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ │ └── SettingsFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── SecretaryApplication.kt │ │ │ │ └── settings │ │ │ │ └── SettingsManager.kt │ │ ├── res │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── drawable │ │ │ │ ├── gradient_background.xml │ │ │ │ ├── ic_send.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── edit_text_background.xml │ │ │ │ ├── bg_message_user.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_memory.xml │ │ │ │ ├── bg_message_assistant.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── ic_mic.xml │ │ │ │ └── ic_settings.xml │ │ │ ├── menu │ │ │ │ ├── menu_chat.xml │ │ │ │ └── menu_main.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── dialog_add_memory.xml │ │ │ │ ├── item_message_user.xml │ │ │ │ ├── item_message_assistant.xml │ │ │ │ ├── item_memory.xml │ │ │ │ ├── fragment_memory.xml │ │ │ │ ├── fragment_chat.xml │ │ │ │ └── fragment_settings.xml │ │ │ ├── navigation │ │ │ │ └── nav_graph.xml │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ └── AndroidManifest.xml │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── aisecretary │ │ └── LlmClientTest.kt └── build.gradle.kts ├── secrets.properties.template ├── settings.gradle.kts ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── CODE_OF_CONDUCT.md ├── gradlew ├── README.md └── LEARN.md /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/A-Akhil/Astra-Ai/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/model/ConversationContext.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.model 2 | 3 | data class ConversationContext( 4 | val recentMessages: List = emptyList(), 5 | val memoryFacts: List = emptyList() 6 | ) -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /secrets.properties.template: -------------------------------------------------------------------------------- 1 | # Copy this file to secrets.properties and fill in your values 2 | # This file is used to provide configuration values at build time without committing them to source control 3 | 4 | # Ollama API configuration 5 | OLLAMA_BASE_URL=https://your-ollama-server-url 6 | LLAMA_MODEL_NAME=llama3:8b -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "ai-secretary-app" 18 | include(":app") -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/rag/DocumentStore.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.rag 2 | 3 | class DocumentStore { 4 | private val documents = mutableListOf() 5 | 6 | fun addDocument(document: String) { 7 | documents.add(document) 8 | } 9 | 10 | fun getDocuments(): List { 11 | return documents 12 | } 13 | 14 | fun clearDocuments() { 15 | documents.clear() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/model/Message.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "messages") 7 | data class Message( 8 | @PrimaryKey(autoGenerate = true) 9 | val id: Long = 0, 10 | val content: String, 11 | val isFromUser: Boolean, 12 | val timestamp: Long = System.currentTimeMillis(), 13 | val isImportantInfo: Boolean = false 14 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/memory/ConversationMemory.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.memory 2 | 3 | class ConversationMemory { 4 | private val memoryStore: MutableList = mutableListOf() 5 | 6 | fun addMemory(memory: String) { 7 | memoryStore.add(memory) 8 | } 9 | 10 | fun getMemories(): List { 11 | return memoryStore 12 | } 13 | 14 | fun clearMemories() { 15 | memoryStore.clear() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/llm/OllamaService.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.llm 2 | 3 | import com.example.aisecretary.ai.llm.model.OllamaRequest 4 | import com.example.aisecretary.ai.llm.model.OllamaResponse 5 | import retrofit2.Response 6 | import retrofit2.http.Body 7 | import retrofit2.http.POST 8 | 9 | interface OllamaService { 10 | @POST("/api/generate") 11 | suspend fun generateCompletion( 12 | @Body request: OllamaRequest 13 | ): Response 14 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_text_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/rag/Retriever.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.rag 2 | 3 | import com.example.aisecretary.ai.rag.DocumentStore 4 | 5 | class Retriever(private val documentStore: DocumentStore) { 6 | 7 | fun retrieveRelevantDocuments(query: String): List { 8 | // Implement logic to retrieve relevant documents based on the query 9 | return documentStore.getDocuments().filter { document -> 10 | document.contains(query, ignoreCase = true) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_message_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/llm/model/OllamaResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.llm.model 2 | 3 | data class OllamaResponse( 4 | val model: String, 5 | val created_at: String, 6 | val response: String, 7 | val done: Boolean, 8 | val context: List? = null, 9 | val total_duration: Long? = null, 10 | val load_duration: Long? = null, 11 | val prompt_eval_count: Int? = null, 12 | val prompt_eval_duration: Long? = null, 13 | val eval_count: Int? = null, 14 | val eval_duration: Long? = null 15 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_memory.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/model/MemoryFact.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import androidx.room.Index 6 | 7 | @Entity( 8 | tableName = "memory_facts", 9 | // No unique constraints - only the primary key (id) should be unique 10 | indices = [Index("key")] // Index on key for faster searching but not unique 11 | ) 12 | data class MemoryFact( 13 | @PrimaryKey(autoGenerate = true) 14 | val id: Long = 0, 15 | val key: String, 16 | val value: String, 17 | val timestamp: Long = System.currentTimeMillis() 18 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_message_assistant.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 13 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/llm/model/OllamaRequest.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.llm.model 2 | 3 | import com.example.aisecretary.BuildConfig 4 | 5 | data class OllamaRequest( 6 | val model: String = BuildConfig.LLAMA_MODEL_NAME, 7 | val prompt: String, 8 | val system: String? = null, 9 | val stream: Boolean = false, 10 | val options: OllamaOptions? = OllamaOptions(), 11 | val keep_alive: Any? = null 12 | ) 13 | 14 | data class OllamaOptions( 15 | val temperature: Float = 0.7f, 16 | val topP: Float = 0.9f, 17 | val topK: Int = 40, 18 | val maxTokens: Int = 800, 19 | val presencePenalty: Float = 0.0f, 20 | val frequencyPenalty: Float = 0.0f 21 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/local/database/dao/MessageDao.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.local.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import com.example.aisecretary.data.model.Message 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface MessageDao { 11 | @Insert 12 | suspend fun insertMessage(message: Message): Long 13 | 14 | @Query("SELECT * FROM messages ORDER BY timestamp ASC") 15 | fun getAllMessages(): Flow> 16 | 17 | @Query("SELECT * FROM messages WHERE isImportantInfo = 1") 18 | fun getImportantInfoMessages(): Flow> 19 | 20 | @Query("DELETE FROM messages") 21 | suspend fun deleteAllMessages() 22 | } -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/local/preferences/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.local.preferences 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | class UserPreferences(context: Context) { 7 | private val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) 8 | 9 | var isVoiceInputEnabled: Boolean 10 | get() = sharedPreferences.getBoolean(KEY_VOICE_INPUT, true) 11 | set(value) = sharedPreferences.edit().putBoolean(KEY_VOICE_INPUT, value).apply() 12 | 13 | var isMemoryStorageEnabled: Boolean 14 | get() = sharedPreferences.getBoolean(KEY_MEMORY_STORAGE, true) 15 | set(value) = sharedPreferences.edit().putBoolean(KEY_MEMORY_STORAGE, value).apply() 16 | 17 | companion object { 18 | private const val PREFS_NAME = "user_preferences" 19 | private const val KEY_VOICE_INPUT = "key_voice_input" 20 | private const val KEY_MEMORY_STORAGE = "key_memory_storage" 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/rag/VectorStore.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.rag 2 | 3 | class VectorStore { 4 | private val vectors = mutableMapOf() 5 | 6 | fun addVector(id: String, vector: FloatArray) { 7 | vectors[id] = vector 8 | } 9 | 10 | fun getVector(id: String): FloatArray? { 11 | return vectors[id] 12 | } 13 | 14 | fun search(queryVector: FloatArray, topK: Int): List> { 15 | return vectors.map { (id, vector) -> 16 | id to cosineSimilarity(queryVector, vector) 17 | }.sortedByDescending { it.second } 18 | .take(topK) 19 | } 20 | 21 | private fun cosineSimilarity(vecA: FloatArray, vecB: FloatArray): Float { 22 | val dotProduct = vecA.zip(vecB).map { (a, b) -> a * b }.sum() 23 | val magnitudeA = Math.sqrt(vecA.map { it * it }.sum().toDouble()).toFloat() 24 | val magnitudeB = Math.sqrt(vecB.map { it * it }.sum().toDouble()).toFloat() 25 | return if (magnitudeA == 0f || magnitudeB == 0f) 0f else dotProduct / (magnitudeA * magnitudeB) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/local/database/dao/MemoryFactDao.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.local.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | import com.example.aisecretary.data.model.MemoryFact 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface MemoryFactDao { 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | suspend fun insertMemoryFact(memoryFact: MemoryFact): Long 15 | 16 | @Update 17 | suspend fun updateMemoryFact(memoryFact: MemoryFact) 18 | 19 | @Query("SELECT * FROM memory_facts ORDER BY timestamp DESC") 20 | fun getAllMemoryFacts(): Flow> 21 | 22 | @Query("SELECT * FROM memory_facts WHERE key LIKE '%' || :query || '%' OR value LIKE '%' || :query || '%'") 23 | fun searchMemoryFacts(query: String): Flow> 24 | 25 | @Query("DELETE FROM memory_facts WHERE id = :id") 26 | suspend fun deleteMemoryFact(id: Long) 27 | 28 | @Query("DELETE FROM memory_facts") 29 | suspend fun deleteAllMemoryFacts() 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | .externalNativeBuild/ 23 | .cxx/ 24 | 25 | # Local configuration file (sdk path, etc) 26 | local.properties 27 | .env 28 | *.env 29 | secrets.properties 30 | /app/src/main/assets/secrets.json 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | .idea/caches 50 | .idea/modules.xml 51 | .idea/navEditor.xml 52 | .idea/misc.xml 53 | 54 | # Keystore files 55 | *.jks 56 | *.keystore 57 | 58 | # Google Services (e.g. APIs or Firebase) 59 | google-services.json 60 | 61 | # Version control 62 | vcs.xml 63 | 64 | # lint 65 | lint/intermediates/ 66 | lint/generated/ 67 | lint/outputs/ 68 | lint/tmp/ 69 | 70 | # MacOS 71 | .DS_Store 72 | 73 | # Windows 74 | Thumbs.db 75 | ehthumbs.db -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/example/aisecretary/LlmClientTest.kt: -------------------------------------------------------------------------------- 1 | import org.junit.Assert.assertEquals 2 | import org.junit.Before 3 | import org.junit.Test 4 | import org.mockito.Mock 5 | import org.mockito.MockitoAnnotations 6 | 7 | class LlamaClientTest { 8 | 9 | @Mock 10 | private lateinit var mockOllamaService: OllamaService 11 | 12 | private lateinit var llamaClient: LlamaClient 13 | 14 | @Before 15 | fun setUp() { 16 | MockitoAnnotations.openMocks(this) 17 | llamaClient = LlamaClient(mockOllamaService) 18 | } 19 | 20 | @Test 21 | fun testGetResponse() { 22 | val userInput = "Hello, how can you assist me?" 23 | val expectedResponse = "I can help you with various tasks." 24 | 25 | // Mock the behavior of the OllamaService 26 | whenever(mockOllamaService.sendRequest(userInput)).thenReturn(expectedResponse) 27 | 28 | val actualResponse = llamaClient.getResponse(userInput) 29 | 30 | assertEquals(expectedResponse, actualResponse) 31 | } 32 | 33 | @Test 34 | fun testGetResponseWithEmptyInput() { 35 | val userInput = "" 36 | val expectedResponse = "Please provide a valid input." 37 | 38 | val actualResponse = llamaClient.getResponse(userInput) 39 | 40 | assertEquals(expectedResponse, actualResponse) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/repository/VoiceRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.repository 2 | 3 | import android.content.Context 4 | import com.example.aisecretary.ai.voice.SpeechRecognizerManager 5 | import com.example.aisecretary.ai.voice.SpeechState 6 | import com.example.aisecretary.ai.voice.TextToSpeechManager 7 | import com.example.aisecretary.ai.voice.TtsState 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.StateFlow 10 | 11 | /** 12 | * Repository that manages voice input/output operations. 13 | */ 14 | class VoiceRepository(private val context: Context) { 15 | 16 | private val speechRecognizerManager = SpeechRecognizerManager(context) 17 | private val textToSpeechManager = TextToSpeechManager(context) 18 | 19 | val speechState: StateFlow = speechRecognizerManager.speechState 20 | val ttsState: StateFlow = textToSpeechManager.ttsState 21 | 22 | init { 23 | speechRecognizerManager.initialize() 24 | } 25 | 26 | fun startListening() { 27 | speechRecognizerManager.startListening() 28 | } 29 | 30 | fun stopListening() { 31 | speechRecognizerManager.stopListening() 32 | } 33 | 34 | fun speak(text: String) { 35 | textToSpeechManager.speak(text) 36 | } 37 | 38 | fun stopSpeaking() { 39 | textToSpeechManager.stop() 40 | } 41 | 42 | fun cleanup() { 43 | speechRecognizerManager.destroy() 44 | textToSpeechManager.shutdown() 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_add_memory.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 24 | 25 | 31 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.di 2 | 3 | import android.content.Context 4 | import android.speech.tts.TextToSpeech 5 | import com.example.aisecretary.BuildConfig 6 | import com.example.aisecretary.SecretaryApplication 7 | import com.example.aisecretary.data.local.database.AppDatabase 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | 11 | /** 12 | * This class provides dependencies throughout the app. 13 | * In a real-world application, you might want to use Dagger/Hilt. 14 | */ 15 | object AppModule { 16 | 17 | // Retrofit instance 18 | fun provideRetrofit(): Retrofit { 19 | // Use a default URL if the BuildConfig.OLLAMA_BASE_URL is empty 20 | val baseUrl = if (BuildConfig.OLLAMA_BASE_URL.isBlank()) { 21 | "http://localhost:11434/" 22 | } else { 23 | // Ensure the URL ends with a slash 24 | if (BuildConfig.OLLAMA_BASE_URL.endsWith("/")) 25 | BuildConfig.OLLAMA_BASE_URL 26 | else 27 | "${BuildConfig.OLLAMA_BASE_URL}/" 28 | } 29 | 30 | return Retrofit.Builder() 31 | .baseUrl(baseUrl) 32 | .addConverterFactory(GsonConverterFactory.create()) 33 | .build() 34 | } 35 | 36 | // Database 37 | fun provideDatabase(context: Context): AppDatabase { 38 | return (context.applicationContext as SecretaryApplication).database 39 | } 40 | 41 | // Text-to-Speech 42 | fun provideTextToSpeech(context: Context, listener: TextToSpeech.OnInitListener): TextToSpeech { 43 | return TextToSpeech(context, listener) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_message_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 26 | 27 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 21 | 22 | 23 | 28 | 29 | 32 | 33 | 34 | 39 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AI Secretary 3 | Welcome to your AI-powered personal secretary! 4 | Type your message or ask a question... 5 | Say something... 6 | Settings 7 | Chat 8 | Memory 9 | An error occurred. Please try again. 10 | Loading... 11 | No messages yet. 12 | Send 13 | Clear Memory 14 | Enable Voice Input 15 | Enable Memory Storage 16 | Save Settings 17 | Speech stopped 18 | Processing first request, please wait... 19 | Triple tap screen to stop speaking 20 | Speaking... 21 | Listening for "Hey Astra"... 22 | Hey Astra detected! 23 | Enable background listening 24 | Listen for "Hey Astra" wake word 25 | Auto-activate microphone after speaking 26 | Automatically start listening after Astra finishes speaking 27 | Cancel 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #4F46E5 5 | #3730A3 6 | #818CF8 7 | 8 | 9 | #0EA5E9 10 | #0284C7 11 | #38BDF8 12 | 13 | 14 | #F9FAFB 15 | #FFFFFF 16 | #1F2937 17 | #111827 18 | #F5F5F5 19 | 20 | 21 | #1F2937 22 | #4B5563 23 | #9CA3AF 24 | #FFFFFF 25 | #FFFFFF 26 | 27 | 28 | #10B981 29 | #F59E0B 30 | #EF4444 31 | #EF4444 32 | #3B82F6 33 | 34 | 35 | #4F46E5 36 | #F3F4F6 37 | #FFFFFF 38 | #1F2937 39 | 40 | 41 | #818CF8 42 | #4F46E5 43 | #3730A3 44 | #38BDF8 45 | #0284C7 46 | #FF000000 47 | #FFFFFFFF 48 | #FFFFFF 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ui/chat/MessageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ui.chat 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.ListAdapter 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.example.aisecretary.R 11 | import com.example.aisecretary.data.model.Message 12 | 13 | class MessageAdapter : ListAdapter(MessageDiffCallback()) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { 16 | val inflater = LayoutInflater.from(parent.context) 17 | val view = when (viewType) { 18 | VIEW_TYPE_USER -> inflater.inflate(R.layout.item_message_user, parent, false) 19 | else -> inflater.inflate(R.layout.item_message_assistant, parent, false) 20 | } 21 | return MessageViewHolder(view) 22 | } 23 | 24 | override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { 25 | val message = getItem(position) 26 | holder.bind(message) 27 | } 28 | 29 | override fun getItemViewType(position: Int): Int { 30 | return if (getItem(position).isFromUser) VIEW_TYPE_USER else VIEW_TYPE_ASSISTANT 31 | } 32 | 33 | companion object { 34 | private const val VIEW_TYPE_USER = 1 35 | private const val VIEW_TYPE_ASSISTANT = 2 36 | } 37 | } 38 | 39 | class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 40 | private val messageText: TextView = itemView.findViewById(R.id.messageText) 41 | 42 | fun bind(message: Message) { 43 | messageText.text = message.content 44 | } 45 | } 46 | 47 | class MessageDiffCallback : DiffUtil.ItemCallback() { 48 | override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean { 49 | return oldItem.id == newItem.id 50 | } 51 | 52 | override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean { 53 | return oldItem == newItem 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_message_assistant.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 22 | 23 | 36 | 37 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.NavController 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.fragment.NavHostFragment 10 | import androidx.navigation.ui.AppBarConfiguration 11 | import androidx.navigation.ui.navigateUp 12 | import androidx.navigation.ui.setupActionBarWithNavController 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private lateinit var navController: NavController 17 | private lateinit var appBarConfiguration: AppBarConfiguration 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_main) 22 | 23 | // Setup toolbar 24 | setSupportActionBar(findViewById(R.id.toolbar)) 25 | 26 | // Setup navigation 27 | val navHostFragment = supportFragmentManager 28 | .findFragmentById(R.id.nav_host_fragment) as NavHostFragment 29 | navController = navHostFragment.navController 30 | 31 | appBarConfiguration = AppBarConfiguration( 32 | setOf(R.id.chatFragment) 33 | ) 34 | setupActionBarWithNavController(navController, appBarConfiguration) 35 | } 36 | 37 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 38 | menuInflater.inflate(R.menu.menu_main, menu) 39 | return true 40 | } 41 | 42 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 43 | return when (item.itemId) { 44 | R.id.action_settings -> { 45 | navController.navigate(R.id.action_chatFragment_to_settingsFragment) 46 | true 47 | } 48 | R.id.action_memory -> { 49 | navController.navigate(R.id.action_chatFragment_to_memoryFragment) 50 | true 51 | } 52 | else -> super.onOptionsItemSelected(item) 53 | } 54 | } 55 | 56 | override fun onSupportNavigateUp(): Boolean { 57 | return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 58 | } 59 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | 6 | # For more details on how to configure your build environment visit 7 | # http://www.gradle.org/docs/current/userguide/build_environment.html 8 | 9 | # Specifies the JVM arguments used for the daemon process. 10 | # The setting is particularly useful for tweaking memory settings. 11 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 12 | 13 | # When configured, Gradle will run in incubating parallel mode. 14 | # This option should only be used with decoupled projects. More details, visit 15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 16 | org.gradle.parallel=true 17 | 18 | # AndroidX package structure to make it clearer which packages are bundled with the 19 | # Android operating system, and which are packaged with your app's APK 20 | android.useAndroidX=true 21 | 22 | # Automatically convert third-party libraries to use AndroidX 23 | android.enableJetifier=true 24 | 25 | # Kotlin code style for this project: "official" or "obsolete": 26 | kotlin.code.style=official 27 | 28 | # Enables namespacing of each library's R class so that its R class includes only the 29 | # resources declared in the library itself and none from the library's dependencies, 30 | # thereby reducing the size of the R class for that library 31 | android.nonTransitiveRClass=true 32 | 33 | # Suppress unsupported compileSdk warning 34 | android.suppressUnsupportedCompileSdk=34 35 | 36 | # Use JDK 17 for the build 37 | org.gradle.java.home=/usr/lib/jvm/java-17-openjdk -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/local/preferences/SettingsManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.local.preferences 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | 9 | /** 10 | * Manages application settings and preferences 11 | */ 12 | class SettingsManager(context: Context) { 13 | 14 | private val sharedPrefs: SharedPreferences = context.getSharedPreferences( 15 | PREFS_NAME, Context.MODE_PRIVATE 16 | ) 17 | 18 | // Memory feature settings 19 | private val _memoryEnabled = MutableStateFlow(isMemoryEnabled()) 20 | val memoryEnabled: Flow = _memoryEnabled.asStateFlow() 21 | 22 | // Voice feature settings 23 | private val _voiceInputEnabled = MutableStateFlow(isVoiceInputEnabled()) 24 | val voiceInputEnabled: Flow = _voiceInputEnabled.asStateFlow() 25 | 26 | private val _voiceOutputEnabled = MutableStateFlow(isVoiceOutputEnabled()) 27 | val voiceOutputEnabled: Flow = _voiceOutputEnabled.asStateFlow() 28 | 29 | // Memory settings 30 | fun isMemoryEnabled(): Boolean { 31 | return sharedPrefs.getBoolean(KEY_MEMORY_ENABLED, true) 32 | } 33 | 34 | fun setMemoryEnabled(enabled: Boolean) { 35 | sharedPrefs.edit().putBoolean(KEY_MEMORY_ENABLED, enabled).apply() 36 | _memoryEnabled.value = enabled 37 | } 38 | 39 | // Voice input settings 40 | fun isVoiceInputEnabled(): Boolean { 41 | return sharedPrefs.getBoolean(KEY_VOICE_INPUT_ENABLED, true) 42 | } 43 | 44 | fun setVoiceInputEnabled(enabled: Boolean) { 45 | sharedPrefs.edit().putBoolean(KEY_VOICE_INPUT_ENABLED, enabled).apply() 46 | _voiceInputEnabled.value = enabled 47 | } 48 | 49 | // Voice output settings 50 | fun isVoiceOutputEnabled(): Boolean { 51 | return sharedPrefs.getBoolean(KEY_VOICE_OUTPUT_ENABLED, true) 52 | } 53 | 54 | fun setVoiceOutputEnabled(enabled: Boolean) { 55 | sharedPrefs.edit().putBoolean(KEY_VOICE_OUTPUT_ENABLED, enabled).apply() 56 | _voiceOutputEnabled.value = enabled 57 | } 58 | 59 | companion object { 60 | private const val PREFS_NAME = "ai_secretary_prefs" 61 | private const val KEY_MEMORY_ENABLED = "memory_enabled" 62 | private const val KEY_VOICE_INPUT_ENABLED = "voice_input_enabled" 63 | private const val KEY_VOICE_OUTPUT_ENABLED = "voice_output_enabled" 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/data/local/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.data.local.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.room.migration.Migration 7 | import androidx.sqlite.db.SupportSQLiteDatabase 8 | import com.example.aisecretary.data.local.database.dao.MessageDao 9 | import com.example.aisecretary.data.local.database.dao.MemoryFactDao 10 | import com.example.aisecretary.data.model.Message 11 | import com.example.aisecretary.data.model.MemoryFact 12 | import android.content.Context 13 | 14 | @Database(entities = [Message::class, MemoryFact::class], version = 2, exportSchema = false) 15 | abstract class AppDatabase : RoomDatabase() { 16 | abstract fun messageDao(): MessageDao 17 | abstract fun memoryFactDao(): MemoryFactDao 18 | 19 | companion object { 20 | @Volatile 21 | private var INSTANCE: AppDatabase? = null 22 | 23 | // Migration from version 1 to 2 24 | val MIGRATION_1_2 = object : Migration(1, 2) { 25 | override fun migrate(database: SupportSQLiteDatabase) { 26 | // Recreate memory_facts table with the index 27 | // First, create a backup of existing data 28 | database.execSQL("CREATE TABLE IF NOT EXISTS `memory_facts_backup` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)") 29 | database.execSQL("INSERT INTO `memory_facts_backup` SELECT * FROM `memory_facts`") 30 | 31 | // Drop the old table 32 | database.execSQL("DROP TABLE IF EXISTS `memory_facts`") 33 | 34 | // Create the new table with the index 35 | database.execSQL("CREATE TABLE IF NOT EXISTS `memory_facts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)") 36 | database.execSQL("CREATE INDEX IF NOT EXISTS `index_memory_facts_key` ON `memory_facts` (`key`)") 37 | 38 | // Restore the data 39 | database.execSQL("INSERT INTO `memory_facts` SELECT * FROM `memory_facts_backup`") 40 | 41 | // Drop the backup table 42 | database.execSQL("DROP TABLE IF EXISTS `memory_facts_backup`") 43 | } 44 | } 45 | 46 | fun getDatabase(context: Context): AppDatabase { 47 | return INSTANCE ?: synchronized(this) { 48 | val instance = Room.databaseBuilder( 49 | context.applicationContext, 50 | AppDatabase::class.java, 51 | "app_database" 52 | ) 53 | .addMigrations(MIGRATION_1_2) 54 | .build() 55 | INSTANCE = instance 56 | instance 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_memory.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 25 | 26 | 35 | 36 | 45 | 46 | 55 | 56 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/SecretaryApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import androidx.room.Room 7 | import com.example.aisecretary.ai.llm.LlamaClient 8 | import com.example.aisecretary.data.local.database.AppDatabase 9 | import com.example.aisecretary.di.AppModule 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.SupervisorJob 13 | import kotlinx.coroutines.launch 14 | 15 | class SecretaryApplication : Application() { 16 | 17 | // Application scope for coroutines 18 | private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 19 | 20 | // Single instance of the database 21 | val database: AppDatabase by lazy { 22 | Room.databaseBuilder( 23 | applicationContext, 24 | AppDatabase::class.java, 25 | "app_database" 26 | ) 27 | .addMigrations(AppDatabase.MIGRATION_1_2) 28 | .fallbackToDestructiveMigration() // This will wipe data if migration fails, but prevents crashes 29 | .build() 30 | } 31 | 32 | // LlamaClient for global access (to unload model if needed) 33 | val llamaClient: LlamaClient by lazy { 34 | LlamaClient(AppModule.provideRetrofit()) 35 | } 36 | 37 | // Track if any activities are running 38 | private var runningActivities = 0 39 | 40 | override fun onCreate() { 41 | super.onCreate() 42 | 43 | // Register to track activity lifecycle to know when app is truly in background 44 | registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { 45 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} 46 | 47 | override fun onActivityStarted(activity: Activity) { 48 | runningActivities++ 49 | } 50 | 51 | override fun onActivityResumed(activity: Activity) {} 52 | override fun onActivityPaused(activity: Activity) {} 53 | 54 | override fun onActivityStopped(activity: Activity) { 55 | runningActivities-- 56 | if (runningActivities <= 0) { 57 | // No activities running - app is in background 58 | // We could unload model here, but it's better to keep it for the 60-minute keep_alive 59 | // as set in LlamaClient to allow quick resume 60 | } 61 | } 62 | 63 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} 64 | 65 | override fun onActivityDestroyed(activity: Activity) { 66 | if (activity.isFinishing && runningActivities <= 1) { 67 | // This is likely the last activity and it's explicitly finishing (not due to config change) 68 | unloadModel() 69 | } 70 | } 71 | }) 72 | } 73 | 74 | private fun unloadModel() { 75 | applicationScope.launch { 76 | llamaClient.unloadModel() 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/ai/voice/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.ai.voice 2 | 3 | import android.content.Context 4 | import android.speech.tts.TextToSpeech 5 | import android.speech.tts.UtteranceProgressListener 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import java.util.* 10 | 11 | /** 12 | * Manages Text-to-Speech functionality for the app. 13 | */ 14 | class TextToSpeechManager( 15 | context: Context 16 | ) { 17 | private var textToSpeech: TextToSpeech? = null 18 | private val _ttsState = MutableStateFlow(TtsState.Idle) 19 | val ttsState: StateFlow = _ttsState.asStateFlow() 20 | 21 | init { 22 | textToSpeech = TextToSpeech(context) { status -> 23 | if (status == TextToSpeech.SUCCESS) { 24 | val result = textToSpeech?.setLanguage(Locale.getDefault()) 25 | if (result == TextToSpeech.LANG_MISSING_DATA || 26 | result == TextToSpeech.LANG_NOT_SUPPORTED 27 | ) { 28 | _ttsState.value = TtsState.Error("Language not supported") 29 | } else { 30 | _ttsState.value = TtsState.Ready 31 | setupTtsListener() 32 | } 33 | } else { 34 | _ttsState.value = TtsState.Error("Initialization failed") 35 | } 36 | } 37 | } 38 | 39 | private fun setupTtsListener() { 40 | textToSpeech?.setOnUtteranceProgressListener(object : UtteranceProgressListener() { 41 | override fun onStart(utteranceId: String?) { 42 | _ttsState.value = TtsState.Speaking 43 | } 44 | 45 | override fun onDone(utteranceId: String?) { 46 | _ttsState.value = TtsState.Ready 47 | } 48 | 49 | @Deprecated("Deprecated in Java") 50 | override fun onError(utteranceId: String?) { 51 | _ttsState.value = TtsState.Error("Error during speech") 52 | } 53 | 54 | override fun onError(utteranceId: String?, errorCode: Int) { 55 | super.onError(utteranceId, errorCode) 56 | _ttsState.value = TtsState.Error("Error code: $errorCode") 57 | } 58 | }) 59 | } 60 | 61 | fun speak(text: String) { 62 | if (_ttsState.value is TtsState.Ready) { 63 | val utteranceId = UUID.randomUUID().toString() 64 | textToSpeech?.speak( 65 | text, 66 | TextToSpeech.QUEUE_FLUSH, 67 | null, 68 | utteranceId 69 | ) 70 | } 71 | } 72 | 73 | fun stop() { 74 | textToSpeech?.stop() 75 | _ttsState.value = TtsState.Ready 76 | } 77 | 78 | fun shutdown() { 79 | textToSpeech?.stop() 80 | textToSpeech?.shutdown() 81 | textToSpeech = null 82 | } 83 | } 84 | 85 | /** 86 | * Represents different states of text-to-speech operations. 87 | */ 88 | sealed class TtsState { 89 | object Idle : TtsState() 90 | object Ready : TtsState() 91 | object Speaking : TtsState() 92 | data class Error(val message: String) : TtsState() 93 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/example/aisecretary/settings/SettingsManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.aisecretary.settings 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | 9 | class SettingsManager(context: Context) { 10 | private val preferences: SharedPreferences = context.getSharedPreferences( 11 | PREFS_NAME, 12 | Context.MODE_PRIVATE 13 | ) 14 | 15 | // Memory enabled setting 16 | private val _memoryEnabled = MutableStateFlow( 17 | preferences.getBoolean(KEY_MEMORY_ENABLED, true) 18 | ) 19 | val memoryEnabled: StateFlow = _memoryEnabled.asStateFlow() 20 | 21 | // Voice output enabled setting 22 | private val _voiceOutputEnabled = MutableStateFlow( 23 | preferences.getBoolean(KEY_VOICE_OUTPUT_ENABLED, true) 24 | ) 25 | val voiceOutputEnabled: StateFlow = _voiceOutputEnabled.asStateFlow() 26 | 27 | // Wake word detection setting - disabled by default 28 | private val _wakeWordEnabled = MutableStateFlow( 29 | preferences.getBoolean(KEY_WAKE_WORD_ENABLED, false) 30 | ) 31 | val wakeWordEnabled: StateFlow = _wakeWordEnabled.asStateFlow() 32 | 33 | // Auto-activate microphone after speaking - enabled by default 34 | private val _autoActivateMic = MutableStateFlow( 35 | preferences.getBoolean(KEY_AUTO_ACTIVATE_MIC, true) 36 | ) 37 | val autoActivateMic: StateFlow = _autoActivateMic.asStateFlow() 38 | 39 | fun isMemoryEnabled(): Boolean { 40 | return preferences.getBoolean(KEY_MEMORY_ENABLED, true) 41 | } 42 | 43 | fun setMemoryEnabled(enabled: Boolean) { 44 | preferences.edit().putBoolean(KEY_MEMORY_ENABLED, enabled).apply() 45 | _memoryEnabled.value = enabled 46 | } 47 | 48 | fun isVoiceOutputEnabled(): Boolean { 49 | return preferences.getBoolean(KEY_VOICE_OUTPUT_ENABLED, true) 50 | } 51 | 52 | fun setVoiceOutputEnabled(enabled: Boolean) { 53 | preferences.edit().putBoolean(KEY_VOICE_OUTPUT_ENABLED, enabled).apply() 54 | _voiceOutputEnabled.value = enabled 55 | } 56 | 57 | fun isWakeWordEnabled(): Boolean { 58 | return preferences.getBoolean(KEY_WAKE_WORD_ENABLED, false) 59 | } 60 | 61 | fun setWakeWordEnabled(enabled: Boolean) { 62 | preferences.edit().putBoolean(KEY_WAKE_WORD_ENABLED, enabled).apply() 63 | _wakeWordEnabled.value = enabled 64 | } 65 | 66 | fun isAutoActivateMicEnabled(): Boolean { 67 | return preferences.getBoolean(KEY_AUTO_ACTIVATE_MIC, true) 68 | } 69 | 70 | fun setAutoActivateMicEnabled(enabled: Boolean) { 71 | preferences.edit().putBoolean(KEY_AUTO_ACTIVATE_MIC, enabled).apply() 72 | _autoActivateMic.value = enabled 73 | } 74 | 75 | companion object { 76 | private const val PREFS_NAME = "secretary_settings" 77 | private const val KEY_MEMORY_ENABLED = "memory_enabled" 78 | private const val KEY_VOICE_OUTPUT_ENABLED = "voice_output_enabled" 79 | private const val KEY_WAKE_WORD_ENABLED = "wake_word_enabled" 80 | private const val KEY_AUTO_ACTIVATE_MIC = "auto_activate_mic" 81 | } 82 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_memory.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 30 | 31 | 40 | 41 |